atris 3.74.14 → 3.74.16

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.
@@ -981,9 +981,39 @@ function inferMemberRunVerifier(missionText, args = [], cwd = process.cwd()) {
981
981
  // chose that check; it is not a silent broad-suite default.
982
982
  const npmMatch = text.match(/\bnpm\s+(run\s+)?test(?![\w:-]|\.\w)/i);
983
983
  if (npmMatch && projectHasTestScript(cwd)) return npmMatch[1] ? 'npm run test' : 'npm test';
984
+ // A named test file in a project that runs node's own test runner: check
985
+ // just that file, since other files may fail on purpose ("legacy.js is out
986
+ // of scope"). Member lab: these jobs fell back to a whitespace check.
987
+ // Only sentences that ask for the work count; "x is out of scope" or "do not
988
+ // run the failing tests" must not pick a check.
989
+ const asks = text.split(/[;,\n]+|\.(?=\s|$)|\s(?:and|but)\s/i).filter((part) => part.trim()
990
+ && !/\b(out\s+of\s+scope|do\s+not|don't|never|without|skip|ignore|leave|except|known\s+bug|reference)\b/i.test(part)).join('. ');
991
+ const testScript = projectTestScript(cwd);
992
+ // The project runs node's test runner itself, not as part of another command.
993
+ if (/^\s*node\s+--test\b/.test(testScript) && !/&&|\|\||[;|]/.test(testScript)) {
994
+ const root = path.resolve(cwd);
995
+ const named = (asks.match(/[\w./-]+\.[cm]?js\b/g) || [])
996
+ .filter((file) => /(^|\/)(tests?|checks?|specs?|__tests__)\/|\.(test|spec)\.[cm]?js$/.test(file))
997
+ .filter((file) => !path.isAbsolute(file) && path.resolve(root, file).startsWith(`${root}${path.sep}`))
998
+ .find((file) => fs.existsSync(path.join(root, file)));
999
+ if (named) return `node --test ${named}`;
1000
+ }
1001
+ // "fix the failing tests" names the project's tests as the target.
1002
+ if (testScript && /\b(fail(ing|ed)?\s+(\w+\s+){0,3}tests?|tests?\s+(pass|green)|fix\s+the\s+(\w+\s+){0,2}tests?)\b/i.test(asks)) {
1003
+ return 'npm test';
1004
+ }
984
1005
  return '';
985
1006
  }
986
1007
 
1008
+ function projectTestScript(cwd) {
1009
+ try {
1010
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
1011
+ return String(pkg?.scripts?.test || '');
1012
+ } catch {
1013
+ return '';
1014
+ }
1015
+ }
1016
+
987
1017
  function projectHasTestScript(cwd) {
988
1018
  try {
989
1019
  const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
@@ -1069,6 +1099,16 @@ function startMemberRunMission(name, missionText, args = []) {
1069
1099
  // second engine only reran the same commands and cost about a minute a run
1070
1100
  // (member lab 2026-10-24), so it stays for runs whose check is not a test
1071
1101
  // runner (whitespace, syntax, or file-exists checks). --no-referee skips it; an explicit --referee is kept.
1102
+ // 'checked' when the run passed, 'unchecked' when the only thing missing is a
1103
+ // reviewer verdict (timed out or could not start), '' otherwise.
1104
+ function memberRunSaveDecision({ started, ok, verdict } = {}) {
1105
+ if (!started) return '';
1106
+ const v = verdict || {};
1107
+ if (v.passed === true) return ok ? 'checked' : '';
1108
+ const reviewerGone = v.referee?.mode === 'engine-unavailable' || v.mode === 'engine-unavailable';
1109
+ return reviewerGone && (v.status === undefined || v.status === 0) ? 'unchecked' : '';
1110
+ }
1111
+
1072
1112
  function memberRunHasRealCheck(verifier) {
1073
1113
  const command = String(verifier || '').trim();
1074
1114
  // A plain test runner only: chaining (|| true, ; , &&, pipes, subshells) or a
@@ -1468,13 +1508,22 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
1468
1508
  }
1469
1509
  // Passed but never committed: save the loose work on the member's branch so
1470
1510
  // the branch holds it. Only in the member's own side checkout, never the main one.
1511
+ // A reviewer that timed out or could not start gave no verdict; the work is
1512
+ // unchecked, not rejected, so save it too (member lab: a correct answer was
1513
+ // lost when the reviewer hit its time limit on a busy machine). Landing still
1514
+ // needs a real pass.
1471
1515
  const worktreePath = finalMission.worktree?.path || mission.worktree?.path || '';
1472
- if (ok && started && worktreePath && finalMission.verifier_result?.passed === true) {
1516
+ const saveDecision = memberRunSaveDecision({ started, ok, verdict: finalMission.verifier_result });
1517
+ const reviewerUnavailable = saveDecision === 'unchecked';
1518
+ if (worktreePath && saveDecision) {
1473
1519
  result.saved_work = require('../lib/member-save-work').saveLeftoverMemberWork({
1474
1520
  worktreePath, member: name, objective: finalMission.objective || mission.objective,
1475
1521
  });
1522
+ if (reviewerUnavailable) result.saved_work.unchecked = true;
1476
1523
  if (result.saved_work.saved && !asJson && !quiet) {
1477
- console.log(`saved ${result.saved_work.files.length} file(s) ${name} left unsaved onto its branch`);
1524
+ console.log(reviewerUnavailable
1525
+ ? `saved ${result.saved_work.files.length} file(s) onto ${name}'s branch, not checked yet: the reviewer timed out or could not start`
1526
+ : `saved ${result.saved_work.files.length} file(s) ${name} left unsaved onto its branch`);
1478
1527
  }
1479
1528
  }
1480
1529
  // A finished ignited run whose check passed hands its branch to GitHub
@@ -12140,6 +12189,7 @@ module.exports = {
12140
12189
  wakeBootLines,
12141
12190
  buildMemberRunStartArgs,
12142
12191
  inferMemberRunVerifier,
12192
+ memberRunSaveDecision,
12143
12193
  buildMemberSubagentShell,
12144
12194
  pickMemberForIdea,
12145
12195
  appendMemberGoalLog,
@@ -9997,6 +9997,9 @@ function buildEngineVerifyPrompt(mission, tickIndex, doneLooksLike = '', { timeo
9997
9997
  `Split that line into each separate thing it promises and check every one. List each part as met or missing.`,
9998
9998
  `Work that does only some of the parts is not done. If any part is missing, end with 'VERDICT: FAIL; missing: <that part>'.`,
9999
9999
  ] : []),
10000
+ // Member lab: reviewers spent about half their time on the workspace boot
10001
+ // (atris atris.md, MAP, PERSONA, skills) before reading the change.
10002
+ `Skip any workspace boot or start-of-session routine the folder's instructions ask for: do not run \`atris atris.md\` and do not read MAP.md, PERSONA.md, FOR_AGENTS.md, or skill files. Start with \`git diff\` and the changed files.`,
10000
10003
  `Read the diff this tick produced. Do not edit files and do not start another work slice.`,
10001
10004
  `ACTUALLY RUN the changed surface on this computer using real commands and real exit codes.`,
10002
10005
  `Paste the commands and their real output. Do not accept a receipt claim or static inspection as execution proof.`,
@@ -90,6 +90,8 @@ button:active:not(:disabled):not(.muted) { opacity: 0.7; }
90
90
  :focus-visible { outline: 2px solid var(--amber); outline-offset: 4px; }
91
91
  /* a link's ring hugs its own tall tap box, round, so it never touches the button above */
92
92
  .links button:focus-visible, .quiet:focus-visible, .bet td button:focus-visible { outline-offset: 0; border-radius: 12px; }
93
+ /* a new card holds the focus itself (landFocus): its ring is round like the page's boxes and clear of the small orb above */
94
+ .bet:focus-visible { outline-offset: 6px; border-radius: 14px; }
93
95
  [hidden] { display: none !important; }
94
96
  /* dark until the page's script has run: a slow link never shows the words without the dots or the card */
95
97
  body:not(.on) .screen, body:not(.on) nav { visibility: hidden; }
@@ -104,9 +106,21 @@ body:not(.on) .screen, body:not(.on) nav { visibility: hidden; }
104
106
  padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
105
107
  display: flex; flex-direction: column; align-items: center; justify-content: center;
106
108
  }
109
+ /* the big orb is 72% of the screen's short side, at most 520px. On a short
110
+ screen (a phone on its side) it is as tall as the room between the top
111
+ margin (--top) and the footer with 8px above it (--foot), once its line
112
+ has its two lines and the 4px above them (55px) */
107
113
  .stage { position: relative; width: min(72vmin, 520px); aspect-ratio: 1 / 1; }
108
- /* on a short screen (a phone on its side) the orb leaves room for its line and the footer */
109
- .stage { width: min(72vmin, 520px, 100svh - 200px); }
114
+ @supports (height: 100svh) {
115
+ .screen {
116
+ --top: max(24px, env(safe-area-inset-top));
117
+ --foot: calc(max(20px, env(safe-area-inset-bottom)) + 44px + 8px);
118
+ --orb: min(72vmin, 520px, 100svh - var(--top) - var(--foot) - 55px);
119
+ /* the orb and its line stay centred, lifted only as far as the line needs to end above the footer */
120
+ padding-bottom: max(24px, env(safe-area-inset-bottom), var(--top) + var(--orb) + 55px + 2 * var(--foot) - 100svh);
121
+ }
122
+ .stage { width: var(--orb); }
123
+ }
110
124
  .stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; touch-action: none; cursor: pointer; }
111
125
  .stage canvas:focus-visible { outline-offset: -8%; }
112
126
  .line {
@@ -147,14 +161,21 @@ body.wide .screen { max-width: 64rem; }
147
161
  /* the team's member header is held with its names, so every header stays over its column */
148
162
  .bet table.k-team th.member { position: sticky; left: 0; z-index: 2; background: var(--bg); }
149
163
  .bet td.member button { font-size: 18px; font-weight: 700; text-align: left; min-height: 32px; }
150
- /* a long name never takes over a phone: the held column stops at about fifteen letters and the name breaks at its hyphens */
151
- .bet table.k-team td.member button { width: max-content; max-width: 8.5em; white-space: normal; overflow-wrap: break-word; }
164
+ /* a long name never takes over a phone: the held column stops at about fifteen letters and the name breaks at its hyphens;
165
+ on the narrowest phones it stops sooner, so the status beside it keeps about 7rem */
166
+ .bet table.k-team td.member button { width: max-content; max-width: min(8.5em, 100vw - 195px); white-space: normal; overflow-wrap: break-word; }
152
167
  .bet td.role { min-width: 9rem; color: var(--text-2); }
153
168
  .bet td.ovr { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
154
169
  .bet th.ovr { text-align: right; }
155
170
  .bet td.engine, .bet td.done { white-space: nowrap; }
156
171
  .bet td.engine button { text-align: left; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 32px; }
157
172
  .bet td.status { min-width: 16rem; color: var(--text-2); }
173
+ /* the team's status is its last column: swiped all the way, it fills the room
174
+ beside the held names (--room, measured) and no more, so every line starts
175
+ beside the names, never under them; with room to spare it keeps 16rem, and
176
+ the table is only as wide as its columns, so no spare width widens it */
177
+ .bet table.k-team { min-width: 0; }
178
+ .bet table.k-team td.status { min-width: min(16rem, var(--room, 16rem)); overflow-wrap: anywhere; }
158
179
  .bet td.status.working { color: var(--text); }
159
180
  .bet td.status.waiting-for-your-yes, .bet td.status.needs-you { color: var(--amber); }
160
181
  .bet td.work { min-width: 14rem; }
@@ -498,8 +519,8 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
498
519
  var CARRY = 0.82;
499
520
  // what the dots did on the last frame, kept on the canvas so the motion can be
500
521
  // measured, and ms: how much of the dots' own clock that frame was, which is
501
- // at most 50ms however long the frame took
502
- var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0 };
522
+ // at most 50ms however long the frame took; dots: how many the sphere has
523
+ var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0, dots: 0 };
503
524
  canvas.__orb = stats;
504
525
  // Two speeds, chosen by truth. A bet already waiting in the queue plays
505
526
  // the same curves at FAST of their length (tap to card in about two seconds, unhurried).
@@ -510,7 +531,17 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
510
531
  var bridgeAt = 0;
511
532
  var REST = 56;
512
533
  var dpr = Math.min(window.devicePixelRatio || 1, 2);
513
- var count = window.innerWidth < 768 ? 5500 : 11000;
534
+ // How many dots, by the size the sphere is drawn at. An upright phone's
535
+ // orb (230 to 320px across) draws 5500 and a laptop's (520px) 11000, as
536
+ // always. A smaller orb (a phone on its side) keeps the spacing of the
537
+ // narrowest phone's, and a bigger one the spacing of the biggest phone's
538
+ // until it reaches 11000, so the sphere is the same light stipple at any
539
+ // size and never a solid ball.
540
+ function dotsFor(size) {
541
+ if (size < 230) return Math.round(5500 * (size / 230) * (size / 230));
542
+ if (size <= 320) return 5500;
543
+ return Math.min(11000, Math.round(5500 * (size / 320) * (size / 320)));
544
+ }
514
545
  var w = 0, h = 0, R = 0, cx = 0, cy = 0;
515
546
  var dots = [];
516
547
  // the dots are drawn by color and brightness, LEVELS of each, so a frame
@@ -546,27 +577,40 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
546
577
  n.style.setProperty('--spin', v[0]); n.style.setProperty('--rot0', v[1]);
547
578
  });
548
579
 
580
+ // the sphere's size on the page, and the dots that size holds
549
581
  function measure() {
550
582
  w = canvas.clientWidth; h = canvas.clientHeight;
551
583
  canvas.width = w * dpr; canvas.height = h * dpr;
552
584
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
553
585
  R = Math.min(w, h) * 0.42; cx = w / 2; cy = h / 2;
586
+ if (w && h) fit(dotsFor(Math.min(w, h)));
554
587
  }
555
588
 
556
- function seed() {
557
- // random points on a sphere (normalised gaussians), so it reads as stipple
589
+ // Grow or trim the sphere to n dots: random points on it (normalised
590
+ // gaussians), so it reads as stipple, and any n of them still do. Every
591
+ // dot kept keeps its place. The first dots start at the centre, so the
592
+ // sphere grows out of the point; one added later starts on the sphere.
593
+ function fit(n) {
594
+ if (n === dots.length) return;
558
595
  var gauss = function () { return Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()); };
559
- dots = [];
560
- px = new Float32Array(count); py = new Float32Array(count); pr = new Float32Array(count); pb = new Int16Array(count); order = new Int32Array(count);
561
- for (var i = 0; i < count; i++) {
596
+ var born = dots.length > 0, cosA = Math.cos(angle), sinA = Math.sin(angle);
597
+ if (n < dots.length) dots.length = n;
598
+ while (dots.length < n) {
562
599
  var gx = gauss(), gy = gauss(), gz = gauss();
563
- var n = Math.hypot(gx, gy, gz) || 1;
564
- dots.push({
565
- x: gx / n, y: gy / n, z: gz / n, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0, kx: 0, ky: 0,
600
+ var len = Math.hypot(gx, gy, gz) || 1;
601
+ var d = {
602
+ x: gx / len, y: gy / len, z: gz / len, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0, kx: 0, ky: 0,
566
603
  // its own slow loop in the swarm
567
604
  f1: 0.5 + Math.random() * 0.8, f2: 0.5 + Math.random() * 0.8, p1: Math.random() * 6.283, p2: Math.random() * 6.283,
568
- });
605
+ };
606
+ if (born) { d.sx = cx + (d.x * cosA + d.z * sinA) * R; d.sy = cy + d.y * R; }
607
+ // mid spiral it goes in with the rest, from where it starts
608
+ d.bx = d.sx; d.by = d.sy;
609
+ d.dl = (1 - Math.min(1, Math.hypot(d.sx - cx, d.sy - cy) / (R || 1))) * 0.28 + Math.random() * 0.05;
610
+ dots.push(d);
569
611
  }
612
+ px = new Float32Array(n); py = new Float32Array(n); pr = new Float32Array(n); pb = new Int16Array(n); order = new Int32Array(n);
613
+ stats.dots = n;
570
614
  }
571
615
 
572
616
  // StippleOrb startBoom: each dot remembers where it starts, and the outer
@@ -909,7 +953,6 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
909
953
  }
910
954
 
911
955
  measure();
912
- seed();
913
956
 
914
957
  // StippleOrb's pointer, ported with trackWindow on, as LetterOrb uses it.
915
958
  // Over the sphere the dots part around the pointer and spring back; the
@@ -950,11 +993,12 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
950
993
  var ratio = Math.min(window.devicePixelRatio || 1, 2);
951
994
  // a phone's bars sliding in and out send resizes too; only a real change redraws
952
995
  if (!canvas.clientWidth || (canvas.clientWidth === w && canvas.clientHeight === h && ratio === dpr)) return;
953
- var was = { cx: cx, cy: cy, R: R };
996
+ var was = { cx: cx, cy: cy, R: R }, kept = dots.length;
954
997
  dpr = ratio;
955
998
  measure();
956
999
  var k = was.R ? R / was.R : 1;
957
- for (var i = 0; i < dots.length; i++) {
1000
+ // the dots it had move with the sphere; any it gained already sit on it
1001
+ for (var i = 0; i < Math.min(kept, dots.length); i++) {
958
1002
  var d = dots[i];
959
1003
  d.sx = cx + (d.sx - was.cx) * k; d.sy = cy + (d.sy - was.cy) * k;
960
1004
  d.bx = cx + (d.bx - was.cx) * k; d.by = cy + (d.by - was.cy) * k;
@@ -1141,9 +1185,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
1141
1185
  var inCells = card.options.filter(function (o) { return o.cell; });
1142
1186
  if (card.table) {
1143
1187
  var grid = b.appendChild(drawTable(card, inCells));
1144
- // whatever scrolls into view in the table lands beside the held names, never under them
1145
- var held = grid.querySelector('tbody td.member');
1146
- if (held && getComputedStyle(held).position === 'sticky') grid.style.scrollPaddingLeft = held.offsetWidth + 'px';
1188
+ holdNames(grid);
1147
1189
  moreRight(grid);
1148
1190
  grid.addEventListener('scroll', function () { moreRight(grid); }, { passive: true });
1149
1191
  }
@@ -1203,7 +1245,16 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
1203
1245
 
1204
1246
  // the fade on a table's right edge, while some of it is still off to the right
1205
1247
  function moreRight(grid) { grid.classList.toggle('more-right', grid.scrollLeft + grid.clientWidth < grid.scrollWidth - 1); }
1206
- window.addEventListener('resize', function () { var g = box().querySelector('.grid'); if (g) moreRight(g); });
1248
+ // whatever scrolls into view in the table lands beside the held names, never
1249
+ // under them, and the room beside them is kept for the last column's width
1250
+ function holdNames(grid) {
1251
+ var held = grid.querySelector('tbody td.member');
1252
+ if (!held || getComputedStyle(held).position !== 'sticky' || !grid.clientWidth) return;
1253
+ grid.style.scrollPaddingLeft = held.offsetWidth + 'px';
1254
+ grid.style.setProperty('--room', (grid.clientWidth - held.offsetWidth) + 'px');
1255
+ }
1256
+ // a turned phone: the room beside the names changes with the screen
1257
+ window.addEventListener('resize', function () { var g = box().querySelector('.grid'); if (g) { holdNames(g); moreRight(g); } });
1207
1258
  // A row of small links that wraps never ends or starts a line with its dot:
1208
1259
  // a dot whose two links sit on different lines becomes the line break.
1209
1260
  function breakLinks(root) {
@@ -2291,17 +2342,21 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
2291
2342
  }, true);
2292
2343
 
2293
2344
  // Where the focus goes. A new card: when the focus left with the old card
2294
- // (or sits on the orb), it goes to the new card's first action, never its
2295
- // typing box, so a keyboard or a screen reader carries on from there. Back
2296
- // on the big orb, a focus that was lost goes to the orb itself.
2345
+ // (or sits on the orb), it goes to the card itself, never to one of its
2346
+ // options or its typing box. An Enter still being pressed (a note just
2347
+ // sent with Enter) then lands on nothing, never on do it or yes, which arm
2348
+ // and then start; a screen reader reads the card from its title, and one
2349
+ // Tab is its first option. Back on the big orb, a focus that was lost (on
2350
+ // a card that has gone) goes to the orb itself.
2297
2351
  function focusFree() {
2298
2352
  var a = document.activeElement;
2299
- return !a || a === document.body || !a.isConnected || a.id === 'orb' || a.id === 'lava';
2353
+ return !a || a === document.body || !a.isConnected || a.id === 'orb' || a.id === 'lava' || (a === box() && !document.body.classList.contains('has-card'));
2300
2354
  }
2355
+ // the card can hold the focus, though Tab never stops on it
2356
+ box().tabIndex = -1;
2301
2357
  function landFocus() {
2302
2358
  if (!document.body.classList.contains('has-card') || !focusFree()) return;
2303
- var first = box().querySelector('button:not([disabled]):not(.send)');
2304
- if (first) first.focus({ preventScroll: true });
2359
+ box().focus({ preventScroll: true });
2305
2360
  }
2306
2361
  new MutationObserver(function () {
2307
2362
  var orbNode = document.getElementById('orb');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.74.14",
3
+ "version": "3.74.16",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "mcpName": "io.github.atrislabs/atris",
6
6
  "main": "bin/atris.js",