staysfixed 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +342 -0
- package/README.md +191 -55
- package/docs/design-v2.md +24 -4
- package/docs/getting-started.md +18 -5
- package/docs/guards.md +2 -2
- package/docs/how-v2-works.md +12 -11
- package/docs/mcp.md +17 -8
- package/docs/settings.md +549 -0
- package/docs/watching.md +10 -4
- package/examples/staysfixed.config.electron.js +17 -6
- package/examples/staysfixed.config.web.js +22 -5
- package/package.json +2 -1
- package/src/cli/index.js +55 -46
- package/src/cli/watch-flags.js +54 -0
- package/src/core/config.js +23 -3
- package/src/guard/run.js +49 -1
- package/src/report/console.js +15 -2
- package/src/v2/adapters/android-driver.js +6 -1
- package/src/v2/adapters/android.js +97 -2
- package/src/v2/adapters/contract.js +42 -5
- package/src/v2/adapters/electron.js +72 -6
- package/src/v2/adapters/http.js +11 -2
- package/src/v2/adapters/ios-driver.js +64 -14
- package/src/v2/adapters/ios.js +247 -25
- package/src/v2/adapters/process.js +728 -66
- package/src/v2/adapters/python.js +495 -0
- package/src/v2/adapters/source.js +373 -18
- package/src/v2/adapters/web-driver.js +94 -24
- package/src/v2/adapters/web.js +142 -9
- package/src/v2/adapters/windows.js +18 -1
- package/src/v2/browsers.js +9 -1
- package/src/v2/cause.js +61 -17
- package/src/v2/check.js +530 -66
- package/src/v2/ci.js +130 -35
- package/src/v2/cli.js +42 -24
- package/src/v2/cluster.js +164 -13
- package/src/v2/coverage.js +43 -176
- package/src/v2/detect.js +308 -60
- package/src/v2/doctor.js +285 -45
- package/src/v2/init.js +162 -61
- package/src/v2/intent.js +9 -23
- package/src/v2/journeys/from-suite.js +336 -30
- package/src/v2/journeys/index.js +99 -6
- package/src/v2/mcp/tools.js +10 -11
- package/src/v2/normalise.js +169 -23
- package/src/v2/observation.js +19 -33
- package/src/v2/rank.js +216 -23
- package/src/v2/reference.js +40 -10
- package/src/v2/remote.js +113 -18
- package/src/v2/run.js +103 -14
- package/src/v2/sealed.js +0 -20
- package/src/v2/selfcheck.js +190 -13
- package/src/v2/ship.js +29 -5
- package/src/v2/store.js +67 -1
- package/src/v2/types.js +12 -2
- package/src/v2/waiver.js +64 -54
- package/src/v2/watch/events.js +60 -215
- package/src/v2/watch/focus.js +14 -4
- package/src/v2/watch/panel.js +167 -17
package/src/v2/watch/events.js
CHANGED
|
@@ -134,6 +134,12 @@ export const SOURCE_WORDS = Object.freeze({
|
|
|
134
134
|
* @property {number} [total] How many journeys there are.
|
|
135
135
|
* @property {number} [count] Whatever this event is counting.
|
|
136
136
|
* @property {number} [watched] Addresses watched so far, across every journey.
|
|
137
|
+
* @property {number} [steady] Only on 'wobble'. Addresses answered the same way
|
|
138
|
+
* twice, as the engine measured them.
|
|
139
|
+
* @property {boolean} [measured] Only on 'wobble'. False when no wobble was taken.
|
|
140
|
+
* @property {string[]} [findingIds] Only on 'check:done', and only when the whole
|
|
141
|
+
* verdict was to hand: every finding that survived,
|
|
142
|
+
* so a window can drop one that has been waived.
|
|
137
143
|
* @property {number} [durationMs]
|
|
138
144
|
* @property {PanelReference} [reference] Only on 'reference'.
|
|
139
145
|
* @property {PanelWobble} [wobble] Only on 'wobble'.
|
|
@@ -235,6 +241,9 @@ export const SOURCE_WORDS = Object.freeze({
|
|
|
235
241
|
* @property {string} summary
|
|
236
242
|
* @property {number} findings
|
|
237
243
|
* @property {number} sealed How many need a person.
|
|
244
|
+
* @property {number} newlyUnstable Addresses that were steady before this change and
|
|
245
|
+
* are not now. A run can be `ok: false` on these
|
|
246
|
+
* alone, with no findings at all.
|
|
238
247
|
* @property {number} differencesReal
|
|
239
248
|
* @property {number} differencesNoise
|
|
240
249
|
* @property {number} durationMs
|
|
@@ -291,220 +300,30 @@ export const SOURCE_WORDS = Object.freeze({
|
|
|
291
300
|
*/
|
|
292
301
|
|
|
293
302
|
// ---------------------------------------------------------------------------
|
|
294
|
-
//
|
|
303
|
+
// WHY THERE IS NO say*() FAMILY HERE ANY MORE (removed 2026-08-30)
|
|
295
304
|
// ---------------------------------------------------------------------------
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
* How many milliseconds in, according to the stream itself when it can say.
|
|
319
|
-
* @param {EventSink|undefined|null} events
|
|
320
|
-
* @returns {number}
|
|
321
|
-
*/
|
|
322
|
-
function now(events) {
|
|
323
|
-
try {
|
|
324
|
-
return typeof events?.elapsed === 'function' ? events.elapsed() : 0;
|
|
325
|
-
} catch {
|
|
326
|
-
return 0;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* The plan, said out loud, for a window that opened after the check started.
|
|
332
|
-
*
|
|
333
|
-
* @param {EventSink|undefined|null} events
|
|
334
|
-
* @param {PanelPlanShape} plan
|
|
335
|
-
* @returns {void}
|
|
336
|
-
*/
|
|
337
|
-
export function sayPlan(events, plan) {
|
|
338
|
-
say(events, { type: 'plan', at: now(events), plan });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Which build this is being measured against — and, when it is the weaker kind, that it is.
|
|
343
|
-
*
|
|
344
|
-
* @param {EventSink|undefined|null} events
|
|
345
|
-
* @param {PanelReference} reference
|
|
346
|
-
* @returns {void}
|
|
347
|
-
*/
|
|
348
|
-
export function sayReference(events, reference) {
|
|
349
|
-
say(events, {
|
|
350
|
-
type: 'reference',
|
|
351
|
-
at: now(events),
|
|
352
|
-
reference,
|
|
353
|
-
message: reference.weak
|
|
354
|
-
? `${reference.warning || 'This is a weaker check than usual.'} ${reference.how}`.trim()
|
|
355
|
-
: `Measured against ${reference.name}. ${reference.how}`.trim(),
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* A journey starting, on a named surface.
|
|
361
|
-
*
|
|
362
|
-
* The surface is the new part. One repository builds a website, a desktop app and a phone app,
|
|
363
|
-
* and a window that only says "walking checkout" leaves a person guessing which of the three
|
|
364
|
-
* they are watching.
|
|
365
|
-
*
|
|
366
|
-
* @param {EventSink|undefined|null} events
|
|
367
|
-
* @param {object} what
|
|
368
|
-
* @param {string} what.journey
|
|
369
|
-
* @param {Surface} [what.surface]
|
|
370
|
-
* @param {string} [what.describe]
|
|
371
|
-
* @param {string} [what.source]
|
|
372
|
-
* @param {string} [what.run] 'a', 'b' or 'single'.
|
|
373
|
-
* @param {number} [what.index]
|
|
374
|
-
* @param {number} [what.total]
|
|
375
|
-
* @returns {void}
|
|
376
|
-
*/
|
|
377
|
-
export function sayJourneyStart(events, what) {
|
|
378
|
-
// "Website: buying one item with a saved card." The surface leads, because on a repository
|
|
379
|
-
// that builds five products the surface is the thing a person is trying to work out.
|
|
380
|
-
const where = what.surface ? `${surfaceWord(what.surface)}: ` : '';
|
|
381
|
-
const doing = what.describe || `walking ${what.journey}`;
|
|
382
|
-
say(events, {
|
|
383
|
-
type: 'journey:start',
|
|
384
|
-
at: now(events),
|
|
385
|
-
journey: what.journey,
|
|
386
|
-
describe: what.describe,
|
|
387
|
-
surface: what.surface,
|
|
388
|
-
surfaceWord: what.surface ? surfaceWord(what.surface) : undefined,
|
|
389
|
-
source: what.source,
|
|
390
|
-
run: what.run,
|
|
391
|
-
index: what.index,
|
|
392
|
-
total: what.total,
|
|
393
|
-
message: `${where}${doing}.`,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* The address count rising while a journey is still walking.
|
|
399
|
-
*
|
|
400
|
-
* This is the number that makes the window worth having open: proof that something is
|
|
401
|
-
* happening, on a run where nothing is going to be wrong and there will be nothing to show.
|
|
402
|
-
*
|
|
403
|
-
* @param {EventSink|undefined|null} events
|
|
404
|
-
* @param {string} journey
|
|
405
|
-
* @param {number} count Addresses this journey has watched so far.
|
|
406
|
-
* @returns {void}
|
|
407
|
-
*/
|
|
408
|
-
export function sayAddresses(events, journey, count) {
|
|
409
|
-
say(events, { type: 'journey:addresses', at: now(events), journey, count });
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* A journey finished.
|
|
414
|
-
* @param {EventSink|undefined|null} events
|
|
415
|
-
* @param {object} what
|
|
416
|
-
* @param {string} what.journey
|
|
417
|
-
* @param {number} what.count Addresses watched.
|
|
418
|
-
* @param {number} [what.unstable] Of those, how many would not sit still.
|
|
419
|
-
* @param {number} [what.durationMs]
|
|
420
|
-
* @param {Surface} [what.surface]
|
|
421
|
-
* @param {string} [what.message]
|
|
422
|
-
* @returns {void}
|
|
423
|
-
*/
|
|
424
|
-
export function sayJourneyDone(events, what) {
|
|
425
|
-
const unstable = Number(what.unstable ?? 0);
|
|
426
|
-
say(events, {
|
|
427
|
-
type: 'journey:done',
|
|
428
|
-
at: now(events),
|
|
429
|
-
journey: what.journey,
|
|
430
|
-
surface: what.surface,
|
|
431
|
-
surfaceWord: what.surface ? surfaceWord(what.surface) : undefined,
|
|
432
|
-
count: what.count,
|
|
433
|
-
durationMs: what.durationMs,
|
|
434
|
-
message:
|
|
435
|
-
what.message ||
|
|
436
|
-
`${plural(what.count, 'address', 'addresses')} watched` +
|
|
437
|
-
(unstable > 0 ? `, ${unstable} of which this build cannot answer the same way twice.` : '.'),
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* The wobble, measured.
|
|
443
|
-
*
|
|
444
|
-
* Nobody else's tool has this number, so it does not get buried. Everything that would not sit
|
|
445
|
-
* still between two runs of the SAME build was not caused by the change, and is subtracted
|
|
446
|
-
* arithmetically rather than allowed for by a tolerance somebody guessed.
|
|
447
|
-
*
|
|
448
|
-
* @param {EventSink|undefined|null} events
|
|
449
|
-
* @param {PanelWobble} wobble
|
|
450
|
-
* @returns {void}
|
|
451
|
-
*/
|
|
452
|
-
export function sayWobble(events, wobble) {
|
|
453
|
-
const note = wobble.note || wobbleSentence(wobble);
|
|
454
|
-
say(events, { type: 'wobble', at: now(events), wobble: { ...wobble, note }, count: wobble.unstable, message: note });
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* One finding, the moment it is formed.
|
|
459
|
-
*
|
|
460
|
-
* The finding goes on the stream whole. Cutting it down is the mapper's job and only the
|
|
461
|
-
* mapper's job — two places trimming the same shape is how a window ends up drawing a
|
|
462
|
-
* finding that has already had its findings taken out of it.
|
|
463
|
-
*
|
|
464
|
-
* @param {EventSink|undefined|null} events
|
|
465
|
-
* @param {Finding} finding
|
|
466
|
-
* @returns {void}
|
|
467
|
-
*/
|
|
468
|
-
export function sayFinding(events, finding) {
|
|
469
|
-
say(events, { type: 'finding', at: now(events), finding, message: finding?.title });
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* The coverage, folded — which is mostly the list of what was NOT looked at.
|
|
474
|
-
* @param {EventSink|undefined|null} events
|
|
475
|
-
* @param {Coverage} coverage
|
|
476
|
-
* @returns {void}
|
|
477
|
-
*/
|
|
478
|
-
export function sayCoverage(events, coverage) {
|
|
479
|
-
const gaps = Array.isArray(coverage?.gaps) ? coverage.gaps.length : 0;
|
|
480
|
-
say(events, {
|
|
481
|
-
type: 'coverage',
|
|
482
|
-
at: now(events),
|
|
483
|
-
coverage,
|
|
484
|
-
count: gaps,
|
|
485
|
-
message: coverageSentence(trimCoverage(coverage)),
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* A plain note, for anything that does not have a shape of its own.
|
|
491
|
-
* @param {EventSink|undefined|null} events
|
|
492
|
-
* @param {string} message
|
|
493
|
-
* @returns {void}
|
|
494
|
-
*/
|
|
495
|
-
export function sayNote(events, message) {
|
|
496
|
-
say(events, { type: 'note', at: now(events), message });
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* The end.
|
|
501
|
-
* @param {EventSink|undefined|null} events
|
|
502
|
-
* @param {Verdict} verdict
|
|
503
|
-
* @returns {void}
|
|
504
|
-
*/
|
|
505
|
-
export function sayCheckDone(events, verdict) {
|
|
506
|
-
say(events, { type: 'check:done', at: now(events), verdict, durationMs: verdict?.durationMs, message: verdict?.summary });
|
|
507
|
-
}
|
|
305
|
+
//
|
|
306
|
+
// This file used to export a second way of talking to a window: sayPlan, sayReference,
|
|
307
|
+
// sayJourneyStart, sayAddresses, sayJourneyDone, sayWobble, sayFinding, sayCoverage,
|
|
308
|
+
// sayNote and sayCheckDone, each one wrapping an emit. Nothing anywhere ever called a
|
|
309
|
+
// single one of them. The engine emits its own plain CheckEvents and `makeMapper` below
|
|
310
|
+
// translates them, so the tool carried two vocabularies for one window and only one of
|
|
311
|
+
// them was wired.
|
|
312
|
+
//
|
|
313
|
+
// They were deleted rather than adopted, and the reason is the paragraph at the top of
|
|
314
|
+
// watch/index.js: the ENGINE works everything out and the PANEL only draws. Every say*()
|
|
315
|
+
// would have had run.js reach for the window's vocabulary at the moment it is meant to be
|
|
316
|
+
// running a difference machine — and it would only have covered the one stream that
|
|
317
|
+
// remembered to call them, where the mapper covers ANY stream, v1's events included. Dead
|
|
318
|
+
// code in a tool whose job is telling the truth about a product is its own small lie: it
|
|
319
|
+
// reads like a supported road and is a road nobody has ever driven down.
|
|
320
|
+
//
|
|
321
|
+
// What was NOT deleted is the vocabulary the window really needs. CLASS_WORDS and
|
|
322
|
+
// SURFACE_NOTES are now embedded in the page by panel.js, exactly the way SURFACE_WORDS
|
|
323
|
+
// and SOURCE_WORDS already were. CLASS_WORDS had to be: the panel was keeping a second
|
|
324
|
+
// hand-written copy of the same map, and the two had already drifted — the panel said "a
|
|
325
|
+
// bug already reported once" where this file says "a bug you already reported", and the
|
|
326
|
+
// panel had no word for 'ordinary' at all.
|
|
508
327
|
|
|
509
328
|
// ---------------------------------------------------------------------------
|
|
510
329
|
// Trimming — what crosses into the window, and what stays out
|
|
@@ -661,6 +480,16 @@ export function trimVerdict(verdict) {
|
|
|
661
480
|
typeof verdict?.sealed === 'number'
|
|
662
481
|
? Number(verdict.sealed)
|
|
663
482
|
: list.filter((/** @type {any} */ f) => f?.sealed || isSealedClass(f?.class)).length;
|
|
483
|
+
// Addresses that used to be steady and are not any more. A run can have NO findings and
|
|
484
|
+
// still not be a pass because of these, and without the number the window cannot tell that
|
|
485
|
+
// apart from a run that compared nothing — the two look identical from `ok: false` and a
|
|
486
|
+
// finding count of nought, and they need opposite sentences.
|
|
487
|
+
const newlyUnstable =
|
|
488
|
+
typeof verdict?.newlyUnstable === 'number'
|
|
489
|
+
? verdict.newlyUnstable
|
|
490
|
+
: Array.isArray(verdict?.newlyUnstable)
|
|
491
|
+
? verdict.newlyUnstable.length
|
|
492
|
+
: 0;
|
|
664
493
|
return {
|
|
665
494
|
ok: Boolean(verdict?.ok),
|
|
666
495
|
mode: verdict?.mode ?? 'stored-record',
|
|
@@ -668,6 +497,7 @@ export function trimVerdict(verdict) {
|
|
|
668
497
|
summary: String(verdict?.summary ?? ''),
|
|
669
498
|
findings: counted,
|
|
670
499
|
sealed,
|
|
500
|
+
newlyUnstable,
|
|
671
501
|
differencesReal: Number(verdict?.differencesReal ?? 0) || 0,
|
|
672
502
|
differencesNoise: Number(verdict?.differencesNoise ?? 0) || 0,
|
|
673
503
|
durationMs: Number(verdict?.durationMs ?? 0) || 0,
|
|
@@ -913,10 +743,17 @@ export function makeMapper(plan = {}) {
|
|
|
913
743
|
|
|
914
744
|
case 'wobble': {
|
|
915
745
|
announcedWobble = true;
|
|
746
|
+
// What the ENGINE measured, wherever it said it. `steady` used to be guessed here as
|
|
747
|
+
// "everything watched so far, minus the unstable ones", and those are two different
|
|
748
|
+
// populations: `watched` counts what the adapters wrote down, while the wobble is
|
|
749
|
+
// measured over addresses. The guess therefore claimed addresses had answered the
|
|
750
|
+
// same way twice that the build had never been asked at. It is kept only as a
|
|
751
|
+
// fallback, for a stream that says nothing about steadiness at all.
|
|
752
|
+
const said = numberOr(event.steady);
|
|
916
753
|
const wobble = /** @type {PanelWobble|undefined} */ (event.wobble) ?? {
|
|
917
|
-
measured:
|
|
754
|
+
measured: event.measured !== false,
|
|
918
755
|
unstable: Number(event.count) || 0,
|
|
919
|
-
steady: Math.max(0, watched - (Number(event.count) || 0)),
|
|
756
|
+
steady: said ?? Math.max(0, watched - (Number(event.count) || 0)),
|
|
920
757
|
newlyUnstable: 0,
|
|
921
758
|
};
|
|
922
759
|
return [{ type: 'wobble', at, message: message ?? wobbleSentence(wobble), wobble, count: wobble.unstable }];
|
|
@@ -993,6 +830,14 @@ export function makeMapper(plan = {}) {
|
|
|
993
830
|
type: 'check:done',
|
|
994
831
|
at,
|
|
995
832
|
verdict: trimVerdict(verdict),
|
|
833
|
+
// WHICH findings, not just how many. A finding only ever ARRIVES at a window; there
|
|
834
|
+
// was no way to take one away again, and findings are taken away — the engine's
|
|
835
|
+
// verdict carries every difference it found, and the gates in check.js then remove
|
|
836
|
+
// the ones an agent has recorded as intended and hand back the settled list. Both
|
|
837
|
+
// verdicts reach the window, in that order, so a waived finding stayed drawn beside
|
|
838
|
+
// a terminal that had already stopped reporting it. Naming the survivors is what
|
|
839
|
+
// lets the window agree: anything drawn that is not on this list is gone.
|
|
840
|
+
findingIds: findings.map((/** @type {any} */ f) => String(f?.id ?? '')).filter(Boolean),
|
|
996
841
|
durationMs: verdict.durationMs,
|
|
997
842
|
message: message ?? verdict.summary,
|
|
998
843
|
});
|
package/src/v2/watch/focus.js
CHANGED
|
@@ -131,19 +131,29 @@ export async function bringForward(name) {
|
|
|
131
131
|
* - When one of ours is in front and you have chosen nothing yet, it is left alone. That
|
|
132
132
|
* first appearance is the point: it is how you see what is happening.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
134
|
+
* `look` and `putBack` exist so this loop can be exercised without a screen. They default to
|
|
135
|
+
* the two functions above and nothing in the tool passes them; a test does, because the
|
|
136
|
+
* bookkeeping — what counts as yours, when the screen is given back, how many times it
|
|
137
|
+
* happened — is the part that has to be right, and it is unreachable behind two calls to
|
|
138
|
+
* `osascript` that answer differently on every machine and not at all on most of them.
|
|
139
|
+
*
|
|
140
|
+
* @param {{claims?: string[], everyMs?: number, graceMs?: number, look?: () => Promise<string|null>, putBack?: (name: string) => Promise<boolean>}} [opts]
|
|
135
141
|
* @returns {ScreenGuard}
|
|
136
142
|
*/
|
|
137
143
|
export function guardTheScreen(opts = {}) {
|
|
138
144
|
const everyMs = opts.everyMs ?? LOOK_EVERY_MS;
|
|
139
145
|
const graceMs = opts.graceMs ?? GRACE_MS;
|
|
146
|
+
const whoIsInFront = opts.look ?? frontmostApp;
|
|
147
|
+
const putBack = opts.putBack ?? bringForward;
|
|
140
148
|
|
|
141
149
|
/** @type {Set<string>} everything the tool opened */
|
|
142
150
|
const ours = new Set(opts.claims ?? []);
|
|
143
151
|
/** @type {string|null} the last application the person chose for themselves */
|
|
144
152
|
let yours = null;
|
|
145
153
|
let handedBack = 0;
|
|
146
|
-
|
|
154
|
+
// Nothing to guard where there is no window server — unless a caller supplied its own way
|
|
155
|
+
// of looking, which means it is being driven deliberately rather than left to the machine.
|
|
156
|
+
let stopped = process.platform !== 'darwin' && !opts.look;
|
|
147
157
|
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
148
158
|
let timer = null;
|
|
149
159
|
const startedAt = Date.now();
|
|
@@ -166,14 +176,14 @@ export function guardTheScreen(opts = {}) {
|
|
|
166
176
|
|
|
167
177
|
const look = async () => {
|
|
168
178
|
if (stopped) return;
|
|
169
|
-
const front = await
|
|
179
|
+
const front = await whoIsInFront();
|
|
170
180
|
if (front) {
|
|
171
181
|
if (!isOurs(front)) {
|
|
172
182
|
// The person chose this. It is now what "yours" means.
|
|
173
183
|
yours = front;
|
|
174
184
|
} else if (yours && Date.now() - startedAt > graceMs) {
|
|
175
185
|
// Something of ours is in front, and there is somewhere to put you back.
|
|
176
|
-
const ok = await
|
|
186
|
+
const ok = await putBack(yours);
|
|
177
187
|
if (ok) {
|
|
178
188
|
handedBack += 1;
|
|
179
189
|
detail(`the screen was taken by ${front}; gave it back to ${yours}`);
|