atris 3.74.18 → 3.74.20

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.
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const { spawnSync } = require('child_process');
7
+ const { localDay } = require('../lib/local-day');
7
8
 
8
9
  const autoland = require('../lib/autoland');
9
10
  const { gateForHuman, plainLandingReason } = require('../lib/voice-gate');
@@ -1006,6 +1007,19 @@ function mergedCloseNote(receipt) {
1006
1007
  + (receipt.merged_close_error ? ', could not check which jobs already merged' : '');
1007
1008
  }
1008
1009
 
1010
+ // The digest goes out once per local day. A stamp written before the switch
1011
+ // to local days is the UTC day of a send at this same local hour, today or
1012
+ // yesterday. Across a clock change both can be the same UTC day; that stamp
1013
+ // cannot say which, so the digest goes out rather than go quiet.
1014
+ function digestSentToday(state, now = new Date()) {
1015
+ if (!state || !state.last_digest_date) return false;
1016
+ if (state.last_digest_local) return state.last_digest_date === localDay(now);
1017
+ const utcDay = (d) => d.toISOString().slice(0, 10);
1018
+ const yesterday = new Date(now);
1019
+ yesterday.setDate(yesterday.getDate() - 1);
1020
+ return state.last_digest_date === utcDay(now) && state.last_digest_date !== utcDay(yesterday);
1021
+ }
1022
+
1009
1023
  async function runTickBody(root, { json, policy, receipt, engineValidationDeps = {} }) {
1010
1024
 
1011
1025
  try {
@@ -1298,9 +1312,9 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
1298
1312
  }
1299
1313
 
1300
1314
  // 4. daily digest at the configured hour
1301
- const today = new Date().toISOString().slice(0, 10);
1315
+ const today = localDay();
1302
1316
  const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
1303
- if (new Date().getHours() === digestHour && state.last_digest_date !== today) {
1317
+ if (new Date().getHours() === digestHour && !digestSentToday(state)) {
1304
1318
  receipt.digest_due = true;
1305
1319
  let waitingWishes = [];
1306
1320
  try {
@@ -1326,6 +1340,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
1326
1340
  }
1327
1341
  receipt.digest_text = text;
1328
1342
  state.last_digest_date = today;
1343
+ state.last_digest_local = true;
1329
1344
  delete state.janitor;
1330
1345
 
1331
1346
  // 5. once a day, keep the receipt shelf lean: compress old run receipts
@@ -1472,6 +1487,7 @@ function autolandCommand(args = []) {
1472
1487
 
1473
1488
  module.exports = {
1474
1489
  autolandCommand,
1490
+ digestSentToday,
1475
1491
  digestNextMoves,
1476
1492
  acceptSweepTimeoutMs,
1477
1493
  describeAcceptKillCause,
@@ -17,6 +17,7 @@ const { loadCredentials } = require('../utils/auth');
17
17
  const { apiRequestJson } = require('../utils/api');
18
18
  const { looksLikeBusinessSlug, requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
19
19
  const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
20
+ const { localDay } = require('../lib/local-day');
20
21
 
21
22
  function sleep(ms) {
22
23
  return new Promise((r) => setTimeout(r, ms));
@@ -58,7 +59,7 @@ async function fetchDailyReport(token, businessId) {
58
59
  }
59
60
 
60
61
  async function writeReportToComputer(token, businessId, workspaceId, report) {
61
- const date = report.date || new Date().toISOString().slice(0, 10);
62
+ const date = report.date || localDay();
62
63
  const remotePath = `/workspace/atris/reports/daily-${date}.md`;
63
64
  const b64 = Buffer.from(report.markdown || '', 'utf8').toString('base64');
64
65
  const command =
@@ -231,4 +232,4 @@ async function fleetReport() {
231
232
  process.exitCode = delivered > 0 || dryRun || results.every((r) => r.reason === 'asleep') ? 0 : 1;
232
233
  }
233
234
 
234
- module.exports = { fleetReport, formatScoreboardPnl };
235
+ module.exports = { fleetReport, formatScoreboardPnl, writeReportToComputer };
@@ -8,6 +8,7 @@ const {
8
8
  } = require('./autopilot');
9
9
  const { detectLessonContradictions } = require('../lib/lesson-contradiction');
10
10
  const taskDb = require('../lib/task-db');
11
+ const { localDay } = require('../lib/local-day');
11
12
  const { validateDetector, appendLedgerEntry, readLedger } = require('../lib/lesson-ledger');
12
13
 
13
14
  /**
@@ -57,7 +58,7 @@ function autoResolveLessons(cwd, options = {}) {
57
58
  const dryRun = !!options.dryRun;
58
59
  const lessons = parseLessons(cwd);
59
60
  const metadata = loadLessonMetadata(cwd);
60
- const today = new Date().toISOString().slice(0, 10);
61
+ const today = localDay();
61
62
  const checked = [];
62
63
  const resolved = [];
63
64
 
@@ -67,10 +67,11 @@ function roundSummary(before, after) {
67
67
  }
68
68
  const fresh = after.missions.filter((m) => !previous.get(m.id)?.has(m.status));
69
69
  const kept = fresh.filter((m) => m.status === 'ended' && m.outcome === 'kept').length;
70
- const failed = fresh.filter((m) => m.status === 'ended' && m.outcome !== 'kept').length;
70
+ const held = fresh.filter((m) => m.status === 'ended' && m.outcome === 'held').length;
71
+ const failed = fresh.filter((m) => m.status === 'ended' && m.outcome !== 'kept' && m.outcome !== 'held').length;
71
72
  const vetoed = fresh.filter((m) => m.status === 'vetoed').length;
72
73
  const spent = Math.round((after.budget.spent - before.budget.spent) * 100) / 100;
73
- return { kept, failed, vetoed, spent, fresh };
74
+ return { kept, failed, held, vetoed, spent, fresh };
74
75
  }
75
76
 
76
77
  // Nothing finished and the check reads the same: there is nothing to learn.
@@ -81,7 +82,7 @@ function quietRound(before, after, summary) {
81
82
 
82
83
  // On a subscription the round is told without dollars, since none were paid.
83
84
  function roundTally(view, summary) {
84
- return `${summary.kept} kept, ${summary.failed} failed${paysDollars(view) ? `, $${summary.spent} spent` : ''}`;
85
+ return `${summary.kept} kept, ${summary.failed} failed${summary.held ? `, ${summary.held} waiting on you` : ''}${paysDollars(view) ? `, $${summary.spent} spent` : ''}`;
85
86
  }
86
87
 
87
88
  function reviewPrompt(view, summary, lessons) {
@@ -94,7 +95,7 @@ function reviewPrompt(view, summary, lessons) {
94
95
  `Campaign: ${view.title}`,
95
96
  `Judge: ${view.judge}`,
96
97
  `Judge now: ${view.last_judge ? `${view.last_judge.passed ? 'passed' : 'failed'}${view.last_judge.reading !== null ? ` (reading ${view.last_judge.reading})` : ''}: ${view.answer.text || ''}` : 'never run'}`,
97
- `This round: ${summary.kept} kept, ${summary.failed} failed, ${summary.vetoed} could not start${paid ? `, $${summary.spent} spent` : ''}`,
98
+ `This round: ${summary.kept} kept, ${summary.failed} failed, ${summary.vetoed} could not start${summary.held ? `, ${summary.held} paused for a person's review` : ''}${paid ? `, $${summary.spent} spent` : ''}`,
98
99
  'Missions this round:',
99
100
  missions,
100
101
  '',
@@ -174,4 +175,4 @@ function makeCadence(root, loop, options = {}, deps = {}) {
174
175
  return { run, review };
175
176
  }
176
177
 
177
- module.exports = { lessonsPath, makeCadence, parseDuration, readLessons, reviewPrompt };
178
+ module.exports = { lessonsPath, makeCadence, parseDuration, readLessons, reviewPrompt, roundSummary, roundTally };
@@ -79,6 +79,8 @@ function rankCampaignMembers(view, rsiSummary = null, events = null) {
79
79
  let streak = 0;
80
80
  for (let i = mine.length - 1; i >= 0; i -= 1) {
81
81
  const outcome = String(mine[i].outcome || '');
82
+ // held waits on a person's yes: it neither adds to a streak nor ends one
83
+ if (outcome === 'held') continue;
82
84
  if (outcome && outcome !== 'kept' && !mine[i].credited) streak += 1;
83
85
  else break;
84
86
  }
@@ -193,8 +193,11 @@ function missionCost(root, missionId, deps = {}) {
193
193
  return { cost_usd: cents / 100, runs: rows.length, uncosted_runs: uncosted };
194
194
  }
195
195
 
196
+ // A mission whose last tick paused for a person's review was held, not
197
+ // failed: the work waits on a yes, so the campaign should say so.
196
198
  function outcomeFor(mission) {
197
- return mission.status === 'complete' ? 'kept' : 'failed';
199
+ if (mission.status === 'complete') return 'kept';
200
+ return mission.last_tick_status === 'paused-for-review' ? 'held' : 'failed';
198
201
  }
199
202
 
200
203
  // Called from saveMission with the record before and after. Only the step
@@ -40,6 +40,12 @@ function dedupeLessons(lines) {
40
40
  return out;
41
41
  }
42
42
 
43
+ // Held is a mission paused for a person's yes: neither kept nor failed, so it
44
+ // adds no failure, streak, or lesson.
45
+ function failedOutcome(outcome) {
46
+ return Boolean(outcome) && outcome !== 'kept' && outcome !== 'held';
47
+ }
48
+
43
49
  function aggregateMembers(missions, failureOrder = missions) {
44
50
  const byMember = new Map();
45
51
  for (const m of missions) {
@@ -48,6 +54,7 @@ function aggregateMembers(missions, failureOrder = missions) {
48
54
  byMember.set(name, {
49
55
  member: name,
50
56
  missions: 0,
57
+ held: 0,
51
58
  kept: 0,
52
59
  failed: 0,
53
60
  reverted: 0,
@@ -63,12 +70,14 @@ function aggregateMembers(missions, failureOrder = missions) {
63
70
  });
64
71
  }
65
72
  const entry = byMember.get(name);
66
- entry.missions += 1;
73
+ // held is not decided yet, so it stays out of the totals a rating divides by
74
+ if (m.outcome === 'held') entry.held += 1;
75
+ else entry.missions += 1;
67
76
  const cost = typeof m.cost_usd === 'number' && Number.isFinite(m.cost_usd) ? m.cost_usd : 0;
68
77
  entry.cost_usd = round(entry.cost_usd + cost, 4);
69
78
  if (m.outcome === 'kept') {
70
79
  entry.kept += 1;
71
- } else if (m.outcome) {
80
+ } else if (failedOutcome(m.outcome)) {
72
81
  entry.failed += 1;
73
82
  if (m.outcome === 'reverted') entry.reverted += 1;
74
83
  entry.wasted_cost_usd = round(entry.wasted_cost_usd + cost, 4);
@@ -84,7 +93,7 @@ function aggregateMembers(missions, failureOrder = missions) {
84
93
  for (const m of failureOrder) {
85
94
  const entry = byMember.get(m.member || 'unknown');
86
95
  if (m.outcome === 'kept') entry.failure_streak = 0;
87
- else if (m.outcome) {
96
+ else if (failedOutcome(m.outcome)) {
88
97
  entry.failure_streak += 1;
89
98
  if (entry.failure_streak >= 2) entry.repeat_failures += 1;
90
99
  }
@@ -223,7 +232,7 @@ function summarizeTrace(events) {
223
232
  `- ${m.member} finished ${m.mission_id} on ${m.campaign_id} with ${m.reward === 0 ? 'no reading gain' : `reading change ${round(m.reward, 4)}`}; tie verify directly to a failing judge check`
224
233
  );
225
234
  }
226
- if (m.outcome && m.outcome !== 'kept') {
235
+ if (failedOutcome(m.outcome)) {
227
236
  rawLessons.push(
228
237
  `- ${m.member} had ${m.mission_id} end ${m.outcome} on ${m.campaign_id}; keep scope to one checkable slice`
229
238
  );
@@ -429,7 +438,7 @@ function writeRsiSnapshot(root, store, campaignId = null) {
429
438
  );
430
439
  const progressFile = path.join(root, 'atris', 'campaigns', cid, 'progress.md');
431
440
  const kept = (c.missions || []).filter((m) => m.outcome === 'kept').length;
432
- const failed = (c.missions || []).filter((m) => m.outcome && m.outcome !== 'kept').length;
441
+ const failed = (c.missions || []).filter((m) => failedOutcome(m.outcome)).length;
433
442
  const progressLines = [
434
443
  `# Progress: ${c.title}`,
435
444
  '',
@@ -559,7 +568,7 @@ function selectNextCampaigns(store, rsiSummary = null) {
559
568
  : rankCampaignMembers(current, rsi, events);
560
569
  const benched = ranked.filter((r) => r.benched).map((r) => r.member);
561
570
  const warm = ranked.find((r) => !r.benched) || ranked[0] || { member: v.owner, overall: 75 };
562
- const failedCount = (v.missions || []).filter((m) => m.status === 'ended' && m.outcome !== 'kept').length;
571
+ const failedCount = (v.missions || []).filter((m) => m.status === 'ended' && m.outcome !== 'kept' && m.outcome !== 'held').length;
563
572
  const recommendation = warm.overall < 65
564
573
  ? `add seat ${topStoreMember} (top ovr) to replace cold lineup (${ranked.map((r) => `${r.member} ${r.overall} ovr`).join(', ')})`
565
574
  : benched.length
@@ -33,8 +33,9 @@ const STATES = Object.freeze([
33
33
  const STOP_REASONS = Object.freeze(['judge_passed', 'stopped', 'done_by_human', 'budget_spent']);
34
34
 
35
35
  // A fault is a job that never ran (engine missing, crash before the first
36
- // step): the setup broke, not the work.
37
- const MISSION_OUTCOMES = Object.freeze(['kept', 'reverted', 'failed', 'fault']);
36
+ // step): the setup broke, not the work. Held is a mission that paused for a
37
+ // person's review before it could finish.
38
+ const MISSION_OUTCOMES = Object.freeze(['kept', 'reverted', 'failed', 'fault', 'held']);
38
39
 
39
40
  // What a person or agent writes. Everything else is folded from events.
40
41
  const CAMPAIGN_FIELDS = Object.freeze({
@@ -1615,12 +1615,28 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
1615
1615
  if (reduced) {
1616
1616
  document.getElementById('lava').style.opacity = '0';
1617
1617
  planned('leave', 200);
1618
- setTimeout(function () { swap(); stageFade(); orb.melt(then, to, function () {}, fast); }, 200);
1618
+ afterFade(b, 200, function () { swap(); stageFade(); orb.melt(then, to, function () {}, fast); });
1619
1619
  return;
1620
1620
  }
1621
1621
  orb.melt(then, to, swap, fast);
1622
1622
  }
1623
1623
 
1624
+ // a fade has run all the way: the node's opacity fade ended, or ms passed
1625
+ // with a frame to spare when none ran. A timer alone cut the last sixth of a
1626
+ // fade that started a frame late on a slow phone, and it vanished at 0.17.
1627
+ function afterFade(node, ms, cb) {
1628
+ var ran = false;
1629
+ var finish = function (e) {
1630
+ // eventPhase 2: the card's own fade, not one bubbling up from inside it
1631
+ if (ran || (e && (e.eventPhase !== 2 || e.propertyName !== 'opacity'))) return;
1632
+ ran = true;
1633
+ node.removeEventListener('transitionend', finish);
1634
+ cb();
1635
+ };
1636
+ node.addEventListener('transitionend', finish);
1637
+ setTimeout(finish, ms + 50);
1638
+ }
1639
+
1624
1640
  // under reduced motion the sphere never travels: it fades in where it now sits
1625
1641
  // and its line fades in with it
1626
1642
  function stageFade() {
@@ -1653,7 +1669,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
1653
1669
  // reduced motion: no spiral; the card fades in where it goes
1654
1670
  if (reduced) {
1655
1671
  var show = function () { orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); };
1656
- if (fadeSphere()) setTimeout(show, 200); else show();
1672
+ if (fadeSphere()) afterFade(document.getElementById('stage'), 200, show); else show();
1657
1673
  return;
1658
1674
  }
1659
1675
  orb.play(function (phase) {
@@ -1951,14 +1967,14 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
1951
1967
  b.classList.add('leave', 'fast');
1952
1968
  // under reduced motion the sphere fades out with the words, then fades in at its own place
1953
1969
  fadeSphere();
1954
- setTimeout(function () {
1970
+ afterFade(b, 220, function () {
1955
1971
  b.hidden = true;
1956
1972
  b.classList.remove('leave', 'fast', 'holding', 'up');
1957
1973
  document.body.classList.remove('has-card', 'wide', 'waiting');
1958
1974
  stageFade();
1959
1975
  document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
1960
1976
  then();
1961
- }, 220);
1977
+ });
1962
1978
  }
1963
1979
 
1964
1980
  // The idea lands: the calm line's words fade while the dots spiral in
@@ -1976,7 +1992,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
1976
1992
  };
1977
1993
  if (reduced) {
1978
1994
  fadeSphere();
1979
- setTimeout(function () { off(); orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); }, 200);
1995
+ afterFade(b, 200, function () { off(); orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); });
1980
1996
  return;
1981
1997
  }
1982
1998
  orb.play(function (phase) {
@@ -2041,7 +2057,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
2041
2057
  b.style.minHeight = '';
2042
2058
  b.classList.remove('up');
2043
2059
  planned('turn', 400);
2044
- setTimeout(function () { paint(); void b.offsetWidth; b.classList.add('up'); setTimeout(after, 200); }, 200);
2060
+ afterFade(b, 200, function () { paint(); void b.offsetWidth; b.classList.add('up'); afterFade(b, 200, after); });
2045
2061
  return;
2046
2062
  }
2047
2063
  var from = b.offsetHeight;
@@ -2183,7 +2199,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
2183
2199
  // the card fades up as the lava eases into its spot, or once it is there
2184
2200
  var up = false;
2185
2201
  var play = function () { orb.play(function (phase) { if (!up && (phase === 'landing' || phase === 'settled')) { up = true; showCard(r.card, true, true); } if (phase === 'settled') bet.busy = false; }, true); };
2186
- if (fadeSphere()) setTimeout(play, 200); else play();
2202
+ if (fadeSphere()) afterFade(document.getElementById('stage'), 200, play); else play();
2187
2203
  }, failed);
2188
2204
  }
2189
2205
 
@@ -4,6 +4,7 @@
4
4
  // 0ms-to-greeting is the contract (atris/features/orb-desktop/PLAN.md).
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const { localDay } = require('./local-day');
7
8
 
8
9
  const MAX_SUGGESTIONS = 12;
9
10
  const WARM_THREAD_WINDOW_MS = 48 * 60 * 60 * 1000;
@@ -37,7 +38,7 @@ function normalizeTime(value) {
37
38
  function todayKey(now) {
38
39
  const stamp = normalizeTime(now);
39
40
  if (!stamp) return '';
40
- return new Date(stamp).toISOString().slice(0, 10);
41
+ return localDay(new Date(stamp));
41
42
  }
42
43
 
43
44
  function normalizeStatus(value) {
@@ -467,7 +468,7 @@ async function appendOrbChoice(root, label, when = new Date()) {
467
468
  const safeRoot = typeof root === 'string' && root ? path.resolve(root) : null;
468
469
  const cleanLabel = cleanTitle(label, 120);
469
470
  if (!safeRoot || !cleanLabel) return { ok: false, error: 'invalid root or label' };
470
- const day = when.toISOString().slice(0, 10);
471
+ const day = localDay(when);
471
472
  const line = `orb: ${cleanLabel} · ${day}\n`;
472
473
  try {
473
474
  const nowPath = fs.existsSync(path.join(safeRoot, 'atris'))
@@ -86,7 +86,9 @@ function writeTaskReceipt({ taskId, command, root = process.cwd(), runner } = {}
86
86
  const dir = runsDir(root);
87
87
  fs.mkdirSync(dir, { recursive: true });
88
88
  const now = new Date();
89
- const date = now.toISOString().slice(0, 10);
89
+ // Local calendar day for the file name; toISOString() is the UTC day,
90
+ // already tomorrow in the evening west of Greenwich.
91
+ const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
90
92
  const safeTime = now.toISOString().replace(/[:.]/g, '-');
91
93
  const receiptFile = path.join(dir, `${date}-task-${slugifyTaskId(taskId)}-${safeTime}.json`);
92
94
  const receiptPath = path.relative(root, receiptFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.74.18",
3
+ "version": "3.74.20",
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",