copilot-operator 0.1.31 → 0.1.33

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 (37) hide show
  1. package/dist/src/api/operator.service.js +10 -2
  2. package/dist/src/orchestrator/derivedChecks.js +34 -1
  3. package/dist/src/orchestrator/review.js +25 -4
  4. package/dist/src/orchestrator/taskRunner.js +44 -15
  5. package/dist/src/plan/importPlan.js +5 -0
  6. package/dist/src/plan/systemGuide.js +8 -0
  7. package/dist/src/transport/copilotTransport.js +70 -36
  8. package/dist/src/transport/locators.js +11 -0
  9. package/dist/web/404.html +2 -2
  10. package/dist/web/_next/static/chunks/867-dfaaa8047cbf2fc2.js +1 -0
  11. package/dist/web/_next/static/chunks/app/defaults/page-54f8f1ab4cac47fa.js +1 -0
  12. package/dist/web/_next/static/chunks/app/sessions/view/page-ce99494b1997ee25.js +1 -0
  13. package/dist/web/appearance.html +2 -2
  14. package/dist/web/appearance.txt +10 -10
  15. package/dist/web/defaults.html +2 -2
  16. package/dist/web/defaults.txt +10 -10
  17. package/dist/web/history.html +2 -2
  18. package/dist/web/history.txt +10 -10
  19. package/dist/web/import.html +2 -2
  20. package/dist/web/import.txt +10 -10
  21. package/dist/web/index.html +2 -2
  22. package/dist/web/index.txt +10 -10
  23. package/dist/web/level1.html +2 -2
  24. package/dist/web/level1.txt +10 -10
  25. package/dist/web/presets.html +2 -2
  26. package/dist/web/presets.txt +10 -10
  27. package/dist/web/sessions/view.html +2 -2
  28. package/dist/web/sessions/view.txt +10 -10
  29. package/dist/web/system.html +2 -2
  30. package/dist/web/system.txt +10 -10
  31. package/package.json +3 -1
  32. package/prompts/review1.md +9 -0
  33. package/dist/web/_next/static/chunks/867-52e371cb1ec49089.js +0 -1
  34. package/dist/web/_next/static/chunks/app/defaults/page-17dd586d1d6bd068.js +0 -1
  35. package/dist/web/_next/static/chunks/app/sessions/view/page-54ab3cb89b84679d.js +0 -1
  36. /package/dist/web/_next/static/{sLgvkKhjVTf3TqVsYNxB7 → G_-u_mF-aLMdcOv0kc9us}/_buildManifest.js +0 -0
  37. /package/dist/web/_next/static/{sLgvkKhjVTf3TqVsYNxB7 → G_-u_mF-aLMdcOv0kc9us}/_ssgManifest.js +0 -0
@@ -398,8 +398,11 @@ let OperatorService = class OperatorService {
398
398
  if (patch.onFailure === 'stop' || patch.onFailure === 'continue')
399
399
  s.onFailure = patch.onFailure;
400
400
  // An empty string is a real choice here: it means "leave the chat on whatever it is".
401
- if (patch.model !== undefined)
401
+ if (patch.model !== undefined) {
402
402
  s.model = patch.model.trim() || undefined;
403
+ // Chosen on the session's page: the operator's own word, which outranks Settings.
404
+ s.modelSource = s.model ? 'operator' : undefined;
405
+ }
403
406
  // The same for the group: clearing it gives this session its conversation back. It does
404
407
  // not move the conversation it is already in; that history is where it is.
405
408
  if (patch.conversationGroup !== undefined)
@@ -408,6 +411,8 @@ let OperatorService = class OperatorService {
408
411
  // Empty model means "the session's own", which is a real choice and not a missing one.
409
412
  const merged = { ...DEFAULT_REVIEW, ...s.review, ...patch.review };
410
413
  s.review = { enabled: merged.enabled !== false, model: (merged.model ?? '').trim() };
414
+ if (patch.review.model !== undefined)
415
+ s.reviewModelSource = s.review.model ? 'operator' : undefined;
411
416
  }
412
417
  if (patch.projectDir !== undefined)
413
418
  s.projectDir = patch.projectDir.trim();
@@ -1339,13 +1344,16 @@ let OperatorService = class OperatorService {
1339
1344
  if (entry.state !== 'waiting')
1340
1345
  continue;
1341
1346
  await this.store.updateSession(entry.sessionId, (s) => {
1342
- if (wantedModel && (s.model?.trim() || wantedModel !== followModel))
1347
+ if (wantedModel && (s.model?.trim() || wantedModel !== followModel)) {
1343
1348
  s.model = wantedModel;
1349
+ s.modelSource = 'operator';
1350
+ }
1344
1351
  // Only the model is set here, never whether the review happens: a run panel is about
1345
1352
  // this run, and silently switching a session's review on or off from it would be a
1346
1353
  // change to the session that outlives the run.
1347
1354
  if (wantedReviewModel && (s.review?.model?.trim() || wantedReviewModel !== followReview)) {
1348
1355
  s.review = { ...DEFAULT_REVIEW, ...s.review, model: wantedReviewModel };
1356
+ s.reviewModelSource = 'operator';
1349
1357
  }
1350
1358
  });
1351
1359
  }
@@ -47,11 +47,16 @@ export function withAttemptIds(reviewChecks) {
47
47
  * Runs each finding's check on the current state. Kept if it fails now, refused if it passes.
48
48
  */
49
49
  export async function validateDerivedChecks(findings, opts) {
50
- const out = { kept: [], refused: [], blocked: [] };
50
+ const out = { kept: [], refused: [], blocked: [], lexical: [] };
51
51
  for (const [i, finding] of findings.entries()) {
52
52
  if (!finding.check)
53
53
  continue;
54
54
  const check = { ...finding.check, name: derivedCheckName(finding.id, finding.check.name) };
55
+ const lexical = selfAttestationReason(check);
56
+ if (lexical) {
57
+ out.lexical.push({ finding, check, why: lexical });
58
+ continue;
59
+ }
55
60
  const outcome = await runCheck(check, 500 + i, {
56
61
  cwd: opts.cwd,
57
62
  logDir: join(opts.logDir, 'derived'),
@@ -71,6 +76,34 @@ export async function validateDerivedChecks(findings, opts) {
71
76
  }
72
77
  return out;
73
78
  }
79
+ /** Words a text uses to vouch for itself. Anyone can write them; they prove nothing about the work. */
80
+ const SELF_ATTESTATION = /\b(?:prove[dn]?|proof|derived|verif(?:y|ied|ies|ication)|validated|confirmed|complete[ds]?|completion|done|finished|tested|passe[sd]|successful(?:ly)?|success|resolved|fixed|activity|evidence|works|working)\b/i;
81
+ /** Commands that only read or search text: what they print is a file's words, not the work running. */
82
+ const TEXT_READ = /^\s*(?:Get-Content|gc|cat|type|more|Select-String|sls|findstr|grep|rg|Get-ChildItem\b.*\|\s*Select-String)\b/i;
83
+ /**
84
+ * Why a check given with a finding is lexical self-attestation, or null when it is not.
85
+ *
86
+ * It is when all it asks is that a text contain a word the text can simply say — "proved", "derived",
87
+ * "verified", "Activity", "complete" — whether by `file-contains` or by reading a file and matching its
88
+ * output. Written so, the check passes the moment the deliverable says the word, and the finding is
89
+ * closed by the claim it was about (operator's feedback, 2026-10-06: "Review findings must not be closed
90
+ * by lexical self-attestation checks"). Text that is structure — a JSON envelope (`"status": "complete"`),
91
+ * an identifier, a path, a number — is not prose and is left alone: those test what the work produced.
92
+ */
93
+ export function selfAttestationReason(check) {
94
+ const value = (check.value ?? '').trim();
95
+ if (!value || !SELF_ATTESTATION.test(value))
96
+ return null;
97
+ // A sentence, identifiers in it included ("bill_type is derived"); JSON, code and paths are structure,
98
+ // evidence of a kind, and left alone.
99
+ if (/[{}[\]()<>=:;"`/\\]/.test(value))
100
+ return null;
101
+ const textPresence = check.expect === 'file-contains' ||
102
+ ((check.expect === 'output-contains' || check.expect === 'output-matches') && TEXT_READ.test(check.run ?? ''));
103
+ if (!textPresence)
104
+ return null;
105
+ return `it only asks that a text contain "${value}", which the text can simply say; a check must test the primary evidence instead`;
106
+ }
74
107
  /** The derived checks that run in the gate. */
75
108
  export function activeChecks(reviewChecks) {
76
109
  return reviewChecks.filter((rc) => rc.state === 'active').map((rc) => rc.check);
@@ -153,9 +153,26 @@ export function reviewBrief(session, task, changedFiles, cwd, deviations = [], p
153
153
  'resolve it at all — a finding that survives a reported fix is often one the task itself made',
154
154
  'unsatisfiable: an instruction a tool overwrites, a setting a version removed.',
155
155
  '',
156
+ 'A finding is resolved by its primary evidence — the artifact exists, the JSON has its envelope,',
157
+ 'the telemetry carries the correlation ID and the rule or output signal, the probe runs and',
158
+ 'succeeds, the cited symbol and path exist — never because the text now says "proved",',
159
+ '"verified", "derived" or "complete". Run the evidence for each one below.',
160
+ '',
156
161
  previous.findings
157
- .map((f, i) => `${i + 1}. ${f.id ? `[${f.id}] ` : ''}${f.what}${f.where ? ` (${f.where})` : ''}${f.repeated ? ' — raised in more than one round already' : ''}`)
158
- .join('\n'),
162
+ .map((f, i) => {
163
+ const check = previous.evidenceChecks?.find((c) => c.findingId === f.id);
164
+ return [
165
+ `${i + 1}. ${f.id ? `[${f.id}] ` : ''}${f.what}${f.where ? ` (${f.where})` : ''}${f.repeated ? ' — raised in more than one round already' : ''}`,
166
+ ...(f.evidence ? [` Evidence then: ${f.evidence}`] : []),
167
+ check
168
+ ? ` Its check "${check.name}": ${check.passed === undefined ? 'not run since' : check.passed ? 'passed at the last gate' : 'FAILED at the last gate'}${check.detail ? ` — ${check.detail}` : ''}${check.state !== 'active' ? ` (${check.state})` : ''}`
169
+ : ' No machine check: judge it on the evidence yourself.',
170
+ ].join('\n');
171
+ })
172
+ .join('\n\n'),
173
+ ...(previous.correction
174
+ ? ['', "The implementer's account of what it corrected (a claim; test it, do not take it):", '', previous.correction.length > 3000 ? `${previous.correction.slice(0, 3000)}…` : previous.correction]
175
+ : []),
159
176
  ...(previous.leftovers && leftoverNote(previous.leftovers)
160
177
  ? ['', leftoverNote(previous.leftovers).replace('After this review', `After round ${previous.round}`), 'Stop what you start, in the same step, and check the port only after that.']
161
178
  : []),
@@ -240,8 +257,8 @@ export async function runReview(transport, session, task, deps) {
240
257
  // The machine's shells, when they are not what the contract's example assumes — the same note
241
258
  // the implementer's conversation opens with, for a conversation working to the same example.
242
259
  const machineNote = shellNote(undefined, defaultShell);
243
- let before = await transport.sendAndConfirm(machineNote ? `${contract}
244
-
260
+ let before = await transport.sendAndConfirm(machineNote ? `${contract}
261
+
245
262
  ${machineNote}` : contract);
246
263
  await transport.waitForReply(before);
247
264
  await pacer.throttleSend();
@@ -407,6 +424,10 @@ ${machineNote}` : contract);
407
424
  markdown = (await transport.waitForReply(b)).markdown;
408
425
  continue;
409
426
  }
427
+ // A check that only asks for a word the text can say closes nothing: the finding stays a finding.
428
+ for (const l of validation.lexical) {
429
+ deps.event('review-check-lexical', { round, finding: l.finding.id, check: l.check.name, value: l.check.value ?? '' }, `the check given with ${l.finding.id} is not kept: ${l.why}. The finding stays, for the next reviewer to judge on the evidence`, 'warn');
430
+ }
410
431
  for (const b of validation.blocked) {
411
432
  deps.event('review-check-blocked', { round, finding: b.finding.id, detail: b.outcome.detail }, `the check given with ${b.finding.id} was refused by the runner and never ran; dropped, the finding stands without it — ${b.outcome.detail}`, 'warn');
412
433
  }
@@ -60,12 +60,13 @@ export const NOT_THE_CHATS = ['contract-conflict', 'environment', 'invalid-check
60
60
  * Records a model the page offers under a new name where it was chosen — the session, or Settings — and
61
61
  * the line-up read on the way. Returns the sentence that says so, or ''.
62
62
  */
63
- async function followPageModels(which, wanted, result, session, store, hooks) {
63
+ async function followPageModels(which, wanted, result, session, store, hooks, from) {
64
64
  if (result.options?.length)
65
65
  await hooks?.seen?.(result.options, result.current).catch(() => undefined);
66
66
  if (!result.ok || !result.matched)
67
67
  return '';
68
- const own = which === 'model' ? session.model?.trim() : session.review?.model?.trim();
68
+ // Written back where it came from: Settings for a model from Settings, the session for the rest.
69
+ const own = from !== 'settings';
69
70
  if (own) {
70
71
  await store
71
72
  ?.updateSession(session.id, (s) => {
@@ -514,14 +515,23 @@ export async function openSessionTransport(cfg, session, bus, runsDir) {
514
515
  * A session gets a model of its own from the plan or from its own picker; one that has none
515
516
  * follows Settings, so a model chosen there after the session was made still reaches it.
516
517
  */
518
+ /**
519
+ * The models a session runs on, and where each came from.
520
+ *
521
+ * The operator's choice on the session's page or the run panel first; then Settings; the plan's last.
522
+ * Settings used to come after any model the session held, and a plan imported with a "model" — written
523
+ * by another chat, often a model the picker no longer had — overrode what the operator chose in Settings
524
+ * on every run (operator's rule, 2026-10-06: what is set in Settings outranks the same thing in the JSON).
525
+ */
517
526
  export function effectiveModels(session, cfg) {
518
- return {
519
- model: (session.model ?? '').trim() || (cfg.copilot.defaultModel ?? '').trim(),
520
- reviewModel: (session.review?.model ?? '').trim() || (cfg.copilot.defaultReviewModel ?? '').trim(),
521
- };
527
+ const pick = (own, source, settings) => own && source === 'operator' ? [own, 'session'] : settings ? [settings, 'settings'] : own ? [own, 'plan'] : ['', 'none'];
528
+ const [model, modelFrom] = pick((session.model ?? '').trim(), session.modelSource, (cfg.copilot.defaultModel ?? '').trim());
529
+ const [reviewModel, reviewModelFrom] = pick((session.review?.model ?? '').trim(), session.reviewModelSource, (cfg.copilot.defaultReviewModel ?? '').trim());
530
+ return { model, reviewModel, modelFrom, reviewModelFrom };
522
531
  }
523
532
  async function applySessionModel(transport, session, bus, cfg, store, hooks) {
524
- const wanted = effectiveModels(session, cfg).model;
533
+ const { model: wanted, modelFrom } = effectiveModels(session, cfg);
534
+ const fromSettings = modelFrom === 'settings';
525
535
  if (!wanted)
526
536
  return {};
527
537
  const locator = await hooks?.locate?.(wanted).catch(() => undefined);
@@ -530,7 +540,7 @@ async function applySessionModel(transport, session, bus, cfg, store, hooks) {
530
540
  current: null,
531
541
  reason: e.message,
532
542
  }));
533
- const renamed = await followPageModels('model', wanted, result, session, store, hooks);
543
+ const renamed = await followPageModels('model', wanted, result, session, store, hooks, modelFrom);
534
544
  if (renamed)
535
545
  bus.publish({ sessionId: session.id, type: 'model-renamed', level: 'warn', message: renamed, data: { asked: wanted, chosen: result.matched } });
536
546
  bus.publish({
@@ -538,9 +548,9 @@ async function applySessionModel(transport, session, bus, cfg, store, hooks) {
538
548
  type: result.ok ? 'model-selected' : 'model-not-selected',
539
549
  level: result.ok ? 'info' : 'warn',
540
550
  message: result.ok
541
- ? `model: ${result.current ?? wanted}${session.model?.trim() ? '' : ' (from Settings)'}`
542
- : `could not switch to "${wanted}"${session.model?.trim() ? '' : ' (the default model in Settings)'}: ${(result.reason ?? 'unknown reason').replace(/[.\s]+$/, '')}. Nothing is sent on ${result.current ?? 'the chat default'} instead.`,
543
- data: { asked: wanted, fromSettings: !session.model?.trim(), current: result.current, ok: result.ok, by: result.by ?? null, savedLocator: !!locator },
551
+ ? `model: ${result.current ?? wanted} (${modelFrom === 'settings' ? 'from Settings' : modelFrom === 'session' ? "the session's own" : 'from the plan'})`
552
+ : `could not switch to "${wanted}" (${modelFrom === 'settings' ? 'the default model in Settings' : modelFrom === 'session' ? "the session's own" : 'from the plan'}): ${(result.reason ?? 'unknown reason').replace(/[.\s]+$/, '')}. Nothing is sent on ${result.current ?? 'the chat default'} instead.`,
553
+ data: { asked: wanted, from: modelFrom, fromSettings, current: result.current, ok: result.ok, by: result.by ?? null, savedLocator: !!locator },
544
554
  });
545
555
  // Kept on the session, so each task's record says which model it ran on and why (see `runTask`).
546
556
  await store
@@ -549,7 +559,7 @@ async function applySessionModel(transport, session, bus, cfg, store, hooks) {
549
559
  asked: wanted,
550
560
  current: result.current ?? null,
551
561
  ok: result.ok,
552
- fromSettings: !session.model?.trim(),
562
+ fromSettings,
553
563
  ...(result.matched ? { renamedTo: result.matched } : {}),
554
564
  ...(result.ok ? {} : { reason: (result.reason ?? 'unknown reason').replace(/[.\s]+$/, '') }),
555
565
  at: new Date().toISOString(),
@@ -563,7 +573,7 @@ async function applySessionModel(transport, session, bus, cfg, store, hooks) {
563
573
  if (!result.ok) {
564
574
  return {
565
575
  current: result.current ?? undefined,
566
- refused: `The chat could not be put on the chosen model "${wanted}"${session.model?.trim() ? '' : ' (Settings)'}: ${(result.reason ?? 'unknown reason').replace(/[.\s]+$/, '')}. ` +
576
+ refused: `The chat could not be put on the chosen model "${wanted}"${fromSettings ? ' (Settings)' : ''}: ${(result.reason ?? 'unknown reason').replace(/[.\s]+$/, '')}. ` +
567
577
  'Nothing was sent. Read the list again under Settings → Model, choose the model there, and start again.',
568
578
  };
569
579
  }
@@ -739,6 +749,8 @@ export async function runTask(transport, session, task, deps) {
739
749
  let reviewChecks = withAttemptIds(task.reviewChecks ?? []);
740
750
  /** Derived checks that still failed when the rounds ran out; the reviewer is told. */
741
751
  let derivedStillFailing = [];
752
+ /** The last gate's result for each check, by name: what a review's findings' checks came to (see `PreviousRound`). */
753
+ const lastGate = new Map();
742
754
  /**
743
755
  * What was running before the task, so that what it and its reviews leave running can be
744
756
  * told apart and stopped. Taken once the working directory is known; null means "do not".
@@ -1485,6 +1497,8 @@ export async function runTask(transport, session, task, deps) {
1485
1497
  };
1486
1498
  runAfterCommit = (cs) => runChecks(cs, checkOptions);
1487
1499
  const ran = await runChecks(checks, checkOptions);
1500
+ for (const o of ran)
1501
+ lastGate.set(o.check.name, { passed: o.passed, detail: o.detail });
1488
1502
  /*
1489
1503
  * The operator stopped the run, which is not a verdict on the work. The check in flight was
1490
1504
  * killed and the ones after it never ran, so what this round "found" is only the Stop: it is
@@ -1744,7 +1758,7 @@ export async function runTask(transport, session, task, deps) {
1744
1758
  if (model) {
1745
1759
  const reviewLocator = await deps.models?.locate?.(model).catch(() => undefined);
1746
1760
  const picked = await transport.selectModel(model, { locator: reviewLocator }).catch((e) => ({ ok: false, current: null, reason: e.message }));
1747
- const renamed = await followPageModels('review', model, picked, session, store, deps.models);
1761
+ const renamed = await followPageModels('review', model, picked, session, store, deps.models, effectiveModels(session, cfg).reviewModelFrom);
1748
1762
  if (renamed)
1749
1763
  sink.event('model-renamed', { asked: model, chosen: picked.matched, review: true }, renamed, 'warn');
1750
1764
  sink.event(picked.ok ? 'review-model-selected' : 'review-model-not-selected', { asked: model, current: picked.current }, picked.ok ? `the review runs on ${picked.current ?? model}` : `the review could not switch to "${model}": ${picked.reason ?? 'unknown reason'}`, picked.ok ? 'info' : 'warn');
@@ -1763,7 +1777,22 @@ export async function runTask(transport, session, task, deps) {
1763
1777
  deliverable,
1764
1778
  deviations,
1765
1779
  disputes,
1766
- previous: reviewRounds > 1 ? { round: reviewRounds - 1, findings: previousFindings, leftovers: previousLeftovers } : undefined,
1780
+ /*
1781
+ * Each earlier finding with its evidence, the correction the implementer reports, and what the
1782
+ * finding's own check came to at the last gate: the next reviewer judges the finding on the evidence,
1783
+ * not on whether the new text says it is fixed (operator's feedback, 2026-10-06).
1784
+ */
1785
+ previous: reviewRounds > 1
1786
+ ? {
1787
+ round: reviewRounds - 1,
1788
+ findings: previousFindings,
1789
+ leftovers: previousLeftovers,
1790
+ correction: closing,
1791
+ evidenceChecks: reviewChecks
1792
+ .filter((rc) => previousFindings.some((f) => f.id === rc.findingId))
1793
+ .map((rc) => ({ findingId: rc.findingId, name: rc.check.name, state: rc.state, ...(lastGate.get(rc.check.name) ?? {}) })),
1794
+ }
1795
+ : undefined,
1767
1796
  repoDir: willCommit ? repoDirOf(session) : undefined,
1768
1797
  derivedFailing: derivedStillFailing,
1769
1798
  // Before the verdict is judged: a check given with a finding must fail on the work,
@@ -232,6 +232,11 @@ defaultProjectDir = '') {
232
232
  s.review = { enabled: planned.review?.enabled !== false, model: (planned.review?.model || defaultReviewModel).trim() };
233
233
  s.conversationGroup = group || undefined;
234
234
  s.model = (planned.model || defaultModel).trim() || undefined;
235
+ // The plan's models are kept, and marked as the plan's: Settings outrank them (see `effectiveModels`).
236
+ if (planned.model?.trim())
237
+ s.modelSource = 'plan';
238
+ if (planned.review?.model?.trim())
239
+ s.reviewModelSource = 'plan';
235
240
  s.planName = plan.plan.trim() || undefined;
236
241
  s.vcs = vcs;
237
242
  // After the plan's own folders are in place, so only a session that named none is pointed at it.
@@ -3004,6 +3004,14 @@ export const SYSTEM_GUIDE = [
3004
3004
  doesEn: 'How many different approaches a "blocked" must list (2–20); with fewer the chat is asked for another.',
3005
3005
  doesBg: 'Колко различни подхода трябва да изброи „blocked“ (2–20); с по-малко чатът е помолен за още един.',
3006
3006
  },
3007
+ {
3008
+ key: 'exec.maxReviewRounds',
3009
+ kind: 'field',
3010
+ en: 'Rounds of fixing after a failed review',
3011
+ bg: 'Кръгове поправка след неуспешен преглед',
3012
+ doesEn: 'How many times a failed review sends its findings back to be fixed and reviewed again (1–6) before the task ends blocked; separate from the approaches above.',
3013
+ doesBg: 'Колко пъти неуспешен преглед връща находките за поправка и нов преглед (1–6), преди задачата да завърши „blocked“; отделно от подходите по-горе.',
3014
+ },
3007
3015
  {
3008
3016
  key: 'exec.retryBlocked',
3009
3017
  kind: 'field',
@@ -10,13 +10,13 @@
10
10
  * 2. Enter does not submit the composer. The Send button has to be clicked.
11
11
  * 3. A message cannot consist of an attachment alone; Send stays disabled without text.
12
12
  */
13
- import { normModel, pageModelFor, sameModel } from './modelMatch.js';
13
+ import { buttonShows, normModel, pageModelFor, sameModel } from './modelMatch.js';
14
14
  import { chromium } from 'playwright';
15
15
  import { mkdir, writeFile } from 'node:fs/promises';
16
16
  import { join } from 'node:path';
17
17
  import { findRecentCrash, describeCrash } from './edgeCrash.js';
18
18
  import { landed } from './acceptance.js';
19
- import { Blocker, Css, Label, Model, OperatorModelLocators, Rename, Sidebar, Signal, Surface, TestId, Upload, Url } from './locators.js';
19
+ import { Blocker, Css, Label, Model, ModelPicking, OperatorModelLocators, Rename, Sidebar, Signal, Surface, TestId, Upload, Url } from './locators.js';
20
20
  import { acquireProfileLock } from './profileLock.js';
21
21
  import { parseChatId } from './chatSession.js';
22
22
  /**
@@ -938,17 +938,23 @@ Current URL: ${url}`);
938
938
  async chooseByOperatorLocators(name, target) {
939
939
  const before = await this.currentModel();
940
940
  const textOf = async (el) => (((await el.innerText().catch(() => '')) ?? '').split('\n')[0] ?? '').trim();
941
+ // A fixed pause after every opening and every press (see `ModelPicking`); the checks give it less.
942
+ const settle = () => this.p.waitForTimeout(this.opts.modelSettleMs ?? ModelPicking.settleMs);
943
+ const appear = this.opts.modelAppearMs ?? ModelPicking.appearMs;
944
+ /** Waits until the element is there and visible. */
945
+ const present = async (el) => await el.waitFor({ state: 'visible', timeout: appear }).then(() => true).catch(() => false);
941
946
  const reach = async () => {
942
947
  const button = await this.resolveModelButton();
943
948
  if (!button)
944
949
  return { problem: 'This chat does not show a model picker.' };
945
950
  await button.click();
951
+ await settle();
946
952
  if (!(await this.waitForPopup()))
947
953
  return { problem: 'The model picker did not open.' };
948
954
  this.emit('model-step', { step: 'picker opened' });
949
955
  if (target.group) {
950
- const group = await this.rowByXPath(target.group.xpath, target.group.name, 5_000);
951
- if (!group)
956
+ const group = await this.rowByXPath(target.group.xpath, target.group.name, appear);
957
+ if (!group || !(await present(group)))
952
958
  return { problem: `the "${target.group.name}" row was not found by ${target.group.xpath}` };
953
959
  this.emit('model-step', { step: 'group found', text: await textOf(group) });
954
960
  /*
@@ -965,8 +971,9 @@ Current URL: ${url}`);
965
971
  ];
966
972
  for (const [how, act] of ways) {
967
973
  await act().catch(() => undefined);
968
- const row = await this.rowByXPath(target.xpath, target.model, 3_000);
969
- if (row) {
974
+ await settle();
975
+ const row = await this.rowByXPath(target.xpath, target.model, appear);
976
+ if (row && (await present(row))) {
970
977
  this.emit('model-step', { step: 'group opened', how, model: await textOf(row) });
971
978
  return { row };
972
979
  }
@@ -975,45 +982,59 @@ Current URL: ${url}`);
975
982
  await this.captureMenu('select-failed', await this.readMenuRows().catch(() => [])).catch(() => undefined);
976
983
  return { problem: `"${target.model}" did not appear after opening "${target.group.name}" every way (${target.xpath}); the menu is kept as models-select-failed.html/.png` };
977
984
  }
978
- const row = await this.rowByXPath(target.xpath, target.model, 5_000);
979
- if (!row) {
985
+ const row = await this.rowByXPath(target.xpath, target.model, appear);
986
+ if (!row || !(await present(row))) {
980
987
  await this.captureMenu('select-failed', await this.readMenuRows().catch(() => [])).catch(() => undefined);
981
988
  return { problem: `"${target.model}" was not found by ${target.xpath}; the menu is kept as models-select-failed.html/.png` };
982
989
  }
983
990
  return { row };
984
991
  };
985
992
  const checked = async (row) => (await row.getAttribute('aria-checked').catch(() => null)) === 'true';
986
- const found = await reach();
987
- if ('problem' in found) {
993
+ /*
994
+ * The whole choice, tried again from a closed menu while the picker does not show the model as chosen,
995
+ * up to `ModelPicking.attempts` times (operator's request, 2026-10-06: "if it is not set, try again").
996
+ */
997
+ const attempts = this.opts.modelAttempts ?? ModelPicking.attempts;
998
+ let why = '';
999
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1000
+ this.emit('model-step', { step: 'attempt', attempt, of: attempts, model: name });
1001
+ const found = await reach();
1002
+ if ('problem' in found) {
1003
+ why = found.problem;
1004
+ this.emit('model-step', { step: 'not reached', attempt, why });
1005
+ await this.closeMenu();
1006
+ await settle();
1007
+ continue;
1008
+ }
1009
+ if (await checked(found.row)) {
1010
+ await this.closeMenu();
1011
+ this.emit('model-already-selected', { model: name, buttonShows: before, by: 'operator', attempt });
1012
+ return { ok: true, current: name, by: 'operator' };
1013
+ }
1014
+ // On the element itself, for the same reason as the group; the mouse only when that does nothing.
1015
+ this.emit('model-step', { step: 'model pressed', attempt, text: await textOf(found.row) });
1016
+ await found.row.evaluate((el) => el.click(), undefined, { timeout: 3_000 }).catch(() => undefined);
1017
+ await settle();
1018
+ if ((await this.modelMenuOpen()) && !(await checked(found.row))) {
1019
+ await found.row.click({ timeout: 3_000 }).catch(() => undefined);
1020
+ await settle();
1021
+ }
988
1022
  await this.closeMenu();
989
- return { ok: false, current: before, reason: found.problem };
990
- }
991
- if (await checked(found.row)) {
1023
+ await settle();
1024
+ // Is it set? Read again, the same way, from a fresh menu.
1025
+ const again = await reach();
1026
+ const ok = 'row' in again && (await checked(again.row));
992
1027
  await this.closeMenu();
993
- this.emit('model-already-selected', { model: name, buttonShows: before, by: 'operator' });
994
- return { ok: true, current: name, by: 'operator' };
995
- }
996
- // On the element itself, for the same reason as the group; the mouse only when that does nothing.
997
- this.emit('model-step', { step: 'model pressed', text: await textOf(found.row) });
998
- await found.row.evaluate((el) => el.click(), undefined, { timeout: 3_000 }).catch(() => undefined);
999
- await this.p.waitForTimeout(800);
1000
- if (await this.modelMenuOpen() && !(await checked(found.row)))
1001
- await found.row.click({ timeout: 3_000 }).catch(() => undefined);
1002
- await this.p.waitForTimeout(400);
1003
- await this.closeMenu();
1004
- const again = await reach();
1005
- const ok = 'row' in again && (await checked(again.row));
1006
- await this.closeMenu();
1007
- const after = await this.currentModel();
1008
- if (ok) {
1009
- this.emit('model-selected', { model: name, buttonShows: after, by: 'operator' });
1010
- return { ok: true, current: name, by: 'operator' };
1028
+ const after = await this.currentModel();
1029
+ if (ok) {
1030
+ this.emit('model-selected', { model: name, buttonShows: after, by: 'operator', attempt });
1031
+ return { ok: true, current: name, by: 'operator' };
1032
+ }
1033
+ why = 'problem' in again ? `after pressing "${target.model}": ${again.problem}` : `"${target.model}" was pressed, but the picker does not mark it as chosen (the button shows "${after ?? 'nothing'}")`;
1034
+ this.emit('model-step', { step: 'not set, trying again', attempt, why });
1035
+ await settle();
1011
1036
  }
1012
- return {
1013
- ok: false,
1014
- current: after,
1015
- reason: 'problem' in again ? `after pressing "${target.model}": ${again.problem}` : `"${target.model}" was pressed, but the picker does not mark it as chosen (the button shows "${after ?? 'nothing'}").`,
1016
- };
1037
+ return { ok: false, current: await this.currentModel(), reason: `not chosen after ${attempts} attempts: ${why}` };
1017
1038
  }
1018
1039
  /**
1019
1040
  * Whether the model picker is open: its button says so (`aria-expanded`), or its rows are on screen.
@@ -1118,6 +1139,17 @@ Current URL: ${url}`);
1118
1139
  const button = await this.resolveModelButton();
1119
1140
  if (!button)
1120
1141
  return { ok: false, current: null, reason: 'This chat does not show a model picker.' };
1142
+ /*
1143
+ * Looked at before anything is opened: a conversation the run comes back to is usually on its model
1144
+ * already, and choosing it again was a menu walk for nothing (operator's request, 2026-10-06). The
1145
+ * button shows the model, shortened by words dropped from its end ("GPT-5.6 Sol Think"), and
1146
+ * `buttonShows` takes only that, so "Think deeper" is never taken for "GPT-5.6 Sol Think deeper".
1147
+ */
1148
+ const shown = await this.currentModel();
1149
+ if (buttonShows(shown, name)) {
1150
+ this.emit('model-already-selected', { model: name, buttonShows: shown, by: 'button' });
1151
+ return { ok: true, current: name, by: 'already' };
1152
+ }
1121
1153
  // The operator's own locators first, for the models they cover (see `OperatorModelLocators`).
1122
1154
  const target = operatorTargetFor(name);
1123
1155
  let first = null;
@@ -1149,6 +1181,8 @@ Current URL: ${url}`);
1149
1181
  const chosen = await this.chooseAt(match.name, match.locator);
1150
1182
  return {
1151
1183
  ...chosen,
1184
+ // When both ways failed, why the operator's locators did is said too: that is the one to fix.
1185
+ ...(!chosen.ok && first?.reason ? { reason: `${chosen.reason ?? 'not chosen'} (${target ? "by the operator's locators" : 'at the saved place'}: ${first.reason})` } : {}),
1152
1186
  ...(chosen.ok && !sameModel(match.name, name) ? { matched: match.name } : {}),
1153
1187
  ...(chosen.ok ? { by: opts.locator ? 'reread' : 'name' } : {}),
1154
1188
  // Why the saved locator missed, kept: what to look at when the page has changed.
@@ -159,6 +159,17 @@ export const OperatorModelLocators = {
159
159
  },
160
160
  ],
161
161
  };
162
+ /**
163
+ * How patiently a model is chosen with those locators (operator's request, 2026-10-06): every element is
164
+ * waited for until it is there and visible (`appearMs`), a fixed pause follows every opening and every
165
+ * press (`settleMs`), and a choice the picker does not show as made is tried again from the start, up to
166
+ * `attempts` times, before the run is told it failed.
167
+ */
168
+ export const ModelPicking = {
169
+ attempts: 6,
170
+ settleMs: 1_500,
171
+ appearMs: 10_000,
172
+ };
162
173
  /** A text as an XPath string literal, whatever quotes it holds. */
163
174
  export function xpathText(text) {
164
175
  if (!text.includes("'"))
package/dist/web/404.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html><!--sLgvkKhjVTf3TqVsYNxB7--><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-52e371cb1ec49089.js" async=""></script><script src="/_next/static/chunks/549-7e35e4fdefadb05d.js" async=""></script><script src="/_next/static/chunks/app/layout-3efbb282292ccc38.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><!--G__u_mF_aLMdcOv0kc9us--><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-dfaaa8047cbf2fc2.js" async=""></script><script src="/_next/static/chunks/549-7e35e4fdefadb05d.js" async=""></script><script src="/_next/static/chunks/app/layout-3efbb282292ccc38.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,&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[8078,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"AppearanceProvider\"]\n3:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"LanguageProvider\"]\n4:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"DialogHost\"]\n5:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"SkipLink\"]\n6:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"\"]\n7:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"Nav\"]\n8:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"GlobalApprovals\"]\n9:I[5204,[],\"\"]\na:I[2642,[],\"\"]\nb:I[7712,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-52e371cb1ec49089.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"ScrollJump\"]\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/c37978b44194c37b.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"sLgvkKhjVTf3TqVsYNxB7\",\"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\":\"\\n(function () {\\n try {\\n var d = document.documentElement;\\n var raw = localStorage.getItem('cop.appearance');\\n var a = raw ? JSON.parse(raw) : {};\\n if (!a || typeof a !== 'object') a = {};\\n d.dataset.theme = a.theme === 'dark' ? 'dark' : 'light';\\n d.dataset.textsize = [\\\"small\\\",\\\"normal\\\",\\\"large\\\",\\\"huge\\\",\\\"giant\\\"].indexOf(a.textSize) \u003e= 0 ? a.textSize : 'normal';\\n d.dataset.contrast = a.highContrast ? 'high' : 'normal';\\n d.dataset.motion = a.reduceMotion ? 'reduced' : 'normal';\\n d.dataset.links = a.underlineLinks ? 'underline' : 'plain';\\n d.dataset.focus = a.strongFocus ? 'strong' : 'normal';\\n } catch (e) {}\\n})();\\n\"}}]}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[[\"$\",\"$L5\",null,{}],[\"$\",\"div\",null,{\"className\":\"wrap\",\"children\":[[\"$\",\"header\",null,{\"className\":\"top\",\"children\":[[\"$\",\"h1\",null,{\"children\":[\"$\",\"$L6\",null,{\"href\":\"/\",\"children\":\"copilot-operator\"}]}],[\"$\",\"$L7\",null,{}]]}],[\"$\",\"$L8\",null,{}],[\"$\",\"main\",null,{\"id\":\"main\",\"children\":[\"$\",\"$L9\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$La\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}],[\"$\",\"$Lb\",null,{}]]}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L9\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$La\",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>
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,&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[8078,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"AppearanceProvider\"]\n3:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"LanguageProvider\"]\n4:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"DialogHost\"]\n5:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"SkipLink\"]\n6:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"\"]\n7:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"Nav\"]\n8:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"GlobalApprovals\"]\n9:I[5204,[],\"\"]\na:I[2642,[],\"\"]\nb:I[7712,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-dfaaa8047cbf2fc2.js\",\"549\",\"static/chunks/549-7e35e4fdefadb05d.js\",\"177\",\"static/chunks/app/layout-3efbb282292ccc38.js\"],\"ScrollJump\"]\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/c37978b44194c37b.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"G_-u_mF-aLMdcOv0kc9us\",\"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\":\"\\n(function () {\\n try {\\n var d = document.documentElement;\\n var raw = localStorage.getItem('cop.appearance');\\n var a = raw ? JSON.parse(raw) : {};\\n if (!a || typeof a !== 'object') a = {};\\n d.dataset.theme = a.theme === 'dark' ? 'dark' : 'light';\\n d.dataset.textsize = [\\\"small\\\",\\\"normal\\\",\\\"large\\\",\\\"huge\\\",\\\"giant\\\"].indexOf(a.textSize) \u003e= 0 ? a.textSize : 'normal';\\n d.dataset.contrast = a.highContrast ? 'high' : 'normal';\\n d.dataset.motion = a.reduceMotion ? 'reduced' : 'normal';\\n d.dataset.links = a.underlineLinks ? 'underline' : 'plain';\\n d.dataset.focus = a.strongFocus ? 'strong' : 'normal';\\n } catch (e) {}\\n})();\\n\"}}]}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[[\"$\",\"$L5\",null,{}],[\"$\",\"div\",null,{\"className\":\"wrap\",\"children\":[[\"$\",\"header\",null,{\"className\":\"top\",\"children\":[[\"$\",\"h1\",null,{\"children\":[\"$\",\"$L6\",null,{\"href\":\"/\",\"children\":\"copilot-operator\"}]}],[\"$\",\"$L7\",null,{}]]}],[\"$\",\"$L8\",null,{}],[\"$\",\"main\",null,{\"id\":\"main\",\"children\":[\"$\",\"$L9\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$La\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}],[\"$\",\"$Lb\",null,{}]]}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L9\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$La\",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>