atris 3.74.0 → 3.74.2

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 (59) hide show
  1. package/bin/atris.js +85 -82
  2. package/commands/aeo.js +2 -2
  3. package/commands/align.js +10 -19
  4. package/commands/auth.js +1 -1
  5. package/commands/autopilot.js +9 -10
  6. package/commands/avail.js +36 -7
  7. package/commands/clean.js +2 -1
  8. package/commands/close.js +7 -4
  9. package/commands/computer.js +3 -1
  10. package/commands/console.js +1 -1
  11. package/commands/context-sync.js +1 -60
  12. package/commands/experiments.js +4 -1
  13. package/commands/fleet.js +0 -1
  14. package/commands/improve.js +3 -1
  15. package/commands/interview.js +13 -5
  16. package/commands/land.js +5 -1
  17. package/commands/lifecycle.js +17 -6
  18. package/commands/log-sync.js +1 -1
  19. package/commands/loops.js +5 -2
  20. package/commands/member.js +17 -11
  21. package/commands/mission.js +1 -4
  22. package/commands/now.js +0 -7
  23. package/commands/play.js +3 -1
  24. package/commands/plugin.js +1 -1
  25. package/commands/pull.js +5 -3
  26. package/commands/push.js +0 -1
  27. package/commands/radar.js +0 -4
  28. package/commands/recap.js +25 -1
  29. package/commands/report.js +0 -4
  30. package/commands/review.js +1 -1
  31. package/commands/security-review.js +4 -1
  32. package/commands/setup.js +6 -2
  33. package/commands/soul.js +2 -1
  34. package/commands/sync.js +8 -2
  35. package/commands/task.js +0 -522
  36. package/commands/terminal.js +1 -1
  37. package/commands/x-search.js +5 -3
  38. package/commands/youtube.js +9 -7
  39. package/commands/zen.js +4 -4
  40. package/lib/agent-today.js +412 -0
  41. package/lib/apply-gate.js +5 -3
  42. package/lib/autoland.js +0 -8
  43. package/lib/bench/runner.js +0 -4
  44. package/lib/chat-log-scan.js +0 -28
  45. package/lib/cli-scope.js +4 -2
  46. package/lib/engine-registry.js +0 -4
  47. package/lib/experiments/daily.js +2 -1
  48. package/lib/file-ops.js +6 -1
  49. package/lib/first-minute.js +2 -0
  50. package/lib/gm-bet.js +73 -9
  51. package/lib/gm-phone-page.js +42 -5
  52. package/lib/gm-phone.js +3 -0
  53. package/lib/local-day.js +10 -0
  54. package/lib/manifest.js +0 -10
  55. package/lib/member-switches.js +0 -11
  56. package/lib/review-integrity.js +0 -4
  57. package/lib/scorecard.js +7 -6
  58. package/lib/wiki.js +4 -1
  59. package/package.json +1 -1
package/lib/gm-bet.js CHANGED
@@ -152,6 +152,8 @@ function readScoreboard(file, workspaceRoot) {
152
152
  if (!row || row.workspace !== workspaceRoot) continue;
153
153
  if (row.kind === 'pick' && row.name) board = { name: row.name, up: row.up !== false, source: row.source === 'installs' ? 'installs' : 'reported', at: row.at, reports: [] };
154
154
  else if (row.kind === 'report' && board && Number.isFinite(row.value)) board.reports.push({ value: row.value, at: row.at });
155
+ // the owner stopped tracking it: no number until one is picked again
156
+ else if (row.kind === 'clear') board = null;
155
157
  }
156
158
  return board;
157
159
  }
@@ -314,6 +316,7 @@ function buildQuestionPrompt(bundle, context, { answers = [], avoid = [] } = {})
314
316
  `- fact: the plain reason this question matters now, one sentence he would say himself, drawn from the files below, with a number or at least four words in a row copied from them, under ${QUESTION_FACT_MAX} characters. it must lead straight to the question. if nothing in the files gives a reason for this question, set fact to "".`,
315
317
  `- choices: 2 or 3 short concrete answers he can tap, each under ${QUESTION_CHOICE_MAX} characters.`,
316
318
  `- never use these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
319
+ '- talk to him as "you", for example "changes you redid". never use his name, and never say "the owner".',
317
320
  '- a section marked stale is old: prefer the fresh ones, and never present a stale line as what is happening now.',
318
321
  '- ask about the product and the people who use it or pay for it, for example "who do you most want using this?". never mention ideas, the team, or what happens to his answer.',
319
322
  '- plain lowercase-friendly words, no hype, no em dashes.',
@@ -344,10 +347,31 @@ function buildQuestionRewrite(previous, reasons) {
344
347
  ].join('\n');
345
348
  }
346
349
 
350
+ // The owner's first name, read at run time from deps or the workspace's git
351
+ // user, so a label a writer wrote about him by name reads to him as "you".
352
+ // Null when there is none to read, and then nothing is swapped.
353
+ function ownerFirstName(workspaceRoot, deps = {}) {
354
+ if (deps.ownerName !== undefined) return deps.ownerName || null;
355
+ try {
356
+ const name = require('child_process').execFileSync('git', ['config', 'user.name'], { cwd: workspaceRoot, timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split(/\s+/)[0];
357
+ return /^[A-Za-z][A-Za-z'-]{1,}$/.test(name) ? name : null;
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+
363
+ function toYou(label, name) {
364
+ if (!name || typeof label !== 'string') return label;
365
+ const esc = name.replace(/[-']/g, m => `\\${m}`);
366
+ return label.replace(new RegExp(`\\b${esc}'s\\b`, 'gi'), 'your').replace(new RegExp(`\\b${esc}\\b`, 'gi'), 'you');
367
+ }
368
+
347
369
  // ---- reading free text: is it a question back, an answer, an instruction? ----
348
370
 
349
371
  const SAY_READ_MS = 15000;
350
372
  const SAY_KINDS = new Set(['question', 'answer', 'instruction', 'unclear']);
373
+ // on the weekly number card, what an instruction wants done with the number
374
+ const NUMBER_INTENTS = new Set(['stop', 'change', 'later', 'other']);
351
375
  const SAY_FALLBACK = 'say that another way, or tap a button.';
352
376
  // the last turns of a card's conversation the reader sees
353
377
  const THREAD_TURNS = 6;
@@ -379,8 +403,10 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
379
403
  '- instruction: he wants something done, or has a new idea. reply: one plain sentence saying what will happen next.',
380
404
  '- unclear: you cannot tell. reply: one short question back.',
381
405
  '- a message that ends with "?" or asks why, what, or how is a question back, never an answer.',
382
- '- you may add "suggestions": 0 to 3 short replies he could tap next, each under 40 characters.',
406
+ '- you may add "suggestions": 0 to 3 short replies he could tap next, each under 40 characters, never one of the buttons already on the card.',
407
+ card.kind === 'number' ? '- on this number card an instruction also has "intent": "stop" when he wants to stop tracking this number, "change" when he wants a different number, "later" when he wants to be asked another time, else "other". for "other", reply says what this card can do.' : '',
383
408
  `- plain lowercase-friendly words, no em dashes, and never these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
409
+ '- talk to him as "you", for example "changes you redid". never use his name, and never say "the owner".',
384
410
  '',
385
411
  `## the card on screen (${card.kind})`,
386
412
  card.title || '',
@@ -404,7 +430,8 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
404
430
  const v = parsed.bet;
405
431
  if (!v || !SAY_KINDS.has(v.kind) || typeof v.reply !== 'string') return { ok: false, reason: 'the read was not the shape asked for' };
406
432
  const suggestions = Array.isArray(v.suggestions) ? v.suggestions.filter(x => typeof x === 'string' && x.trim()).map(x => x.trim()) : [];
407
- return { ok: true, kind: v.kind, reply: clip(v.reply, 280), ...(suggestions.length ? { suggestions } : {}) };
433
+ const intent = card.kind === 'number' && NUMBER_INTENTS.has(v.intent) ? v.intent : null;
434
+ return { ok: true, kind: v.kind, reply: clip(v.reply, 280), ...(intent ? { intent } : {}), ...(suggestions.length ? { suggestions } : {}) };
408
435
  }
409
436
 
410
437
  // Two or three numbers that would tell the owner this project is going well,
@@ -418,6 +445,7 @@ async function generateScoreboardChoices({ workspaceRoot, bundle = null, deps =
418
445
  'Answer with strict JSON only, no prose and no code fence: {"choices": [{"name": "...", "up": true}]}',
419
446
  '- name: a short plain phrase under 40 characters. up: true when higher is better, false when lower is better.',
420
447
  `- never use these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
448
+ '- talk to him as "you", for example "changes you redid". never use his name, and never say "the owner".',
421
449
  '',
422
450
  "## this project's files",
423
451
  context || '(no readme or goals found)',
@@ -2702,17 +2730,18 @@ function createBetDesk(deps) {
2702
2730
  const board = readScoreboard(scoreboardFile, deps.workspaceRoot);
2703
2731
  if (!board) {
2704
2732
  if (state.passedQuestions.has('scoreboard')) return null;
2705
- const list = (state.suggestions || fallbackNumbers()).slice(0, 3);
2733
+ const list = (state.suggestions || fallbackNumbers()).slice(0, 3).map(c => ({ ...c, name: you(c.name) }));
2706
2734
  return {
2707
2735
  kind: 'question',
2708
2736
  id: 'question:scoreboard',
2709
2737
  question: { key: 'scoreboard', ask: SCOREBOARD_ASK, line: null, choices: [...list.map((c, i) => ({ id: `s${i + 1}`, label: c.name })), { id: 'other', label: 'something else', other: true }], numbers: list },
2710
2738
  };
2711
2739
  }
2712
- if (board.source !== 'reported' || state.passedQuestions.has('report')) return null;
2740
+ const today = dayKey(now());
2741
+ // passed over today (not now, or a tap past it): asked again tomorrow
2742
+ if (board.source !== 'reported' || state.passedQuestions.has(`report:${today}`)) return null;
2713
2743
  const last = board.reports[board.reports.length - 1] || null;
2714
2744
  const weekly = !last || now() - (Date.parse(last.at) || 0) >= WEEK_MS;
2715
- const today = dayKey(now());
2716
2745
  const waiting = readLedger(deps.ledgerFile, deps.workspaceRoot).some(r => r.status === 'doing' && r.bet && r.bet.target && r.bet.target.metric === 'scoreboard'
2717
2746
  && String(r.bet.check_date || '') < today && !board.reports.some(x => String(x.at).slice(0, 10) >= r.bet.check_date));
2718
2747
  if (!weekly && !waiting) return null;
@@ -2805,7 +2834,7 @@ function createBetDesk(deps) {
2805
2834
  const row = readyQuestions().find(r => !state.passedQuestions.has(r.id));
2806
2835
  if (row) {
2807
2836
  const q = row.question;
2808
- return { kind: 'question', id: `question:${row.id}`, question: { key: row.id, ask: q.ask, line: q.fact, choices: q.choices.map((label, i) => ({ id: `a${i + 1}`, label })) } };
2837
+ return { kind: 'question', id: `question:${row.id}`, question: { key: row.id, ask: q.ask, line: q.fact, choices: q.choices.map((label, i) => ({ id: `a${i + 1}`, label: you(label) })) } };
2809
2838
  }
2810
2839
  const F = FALLBACK_QUESTION;
2811
2840
  const said = readFocus(focusFile, deps.workspaceRoot)[F.key];
@@ -2860,7 +2889,7 @@ function createBetDesk(deps) {
2860
2889
  // a re-tap on a job card moves on: a yes waits for a later session, a done is seen
2861
2890
  function passJob(item) {
2862
2891
  if (item.kind === 'wait') return;
2863
- if (item.kind === 'report') { state.passedQuestions.add('report'); return; }
2892
+ if (item.kind === 'report') { state.passedQuestions.add(item.id); return; }
2864
2893
  if (item.kind === 'choice') {
2865
2894
  for (const i of item.items) record(i.id, { status: 'passed', note: 'not picked' });
2866
2895
  log({ card: 'bet', choice: 'pass', command: null, ran: 'in process', exit_code: 0, kind: 'choice' });
@@ -3259,6 +3288,9 @@ function createBetDesk(deps) {
3259
3288
  t.turns = [...t.turns, ...turns].slice(-THREAD_TURNS);
3260
3289
  }
3261
3290
  const plainReply = text => (text && !ownerWord(text) ? text : null);
3291
+ let ownerName;
3292
+ const you = label => toYou(label, ownerName === undefined ? (ownerName = ownerFirstName(deps.workspaceRoot, deps)) : ownerName);
3293
+ const sameLabel = (a, b) => String(a).trim().toLowerCase() === String(b).trim().toLowerCase();
3262
3294
  const plainSuggestions = list => (Array.isArray(list) ? list : [])
3263
3295
  .filter(x => typeof x === 'string' && x.trim() && x.length <= 40 && !ownerWord(x) && !PROCESS_TALK.test(x))
3264
3296
  .slice(0, 3);
@@ -3279,7 +3311,7 @@ function createBetDesk(deps) {
3279
3311
  if (v.kind === 'question') return { kind: 'question', title: v.question.ask, detail: v.question.line, buttons: v.question.choices.map(c => c.label) };
3280
3312
  if (v.kind === 'bet') return { kind: 'idea', title: v.bet.title, detail: [v.bet.why_now, v.bet.expect].join(' '), buttons: ['do it', 'sharper idea', 'not now'] };
3281
3313
  if (v.kind === 'choice') return { kind: 'choice', title: 'which one next?', detail: v.choice.options.map(o => o.title).join(' / '), buttons: [] };
3282
- if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: [] };
3314
+ if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: ['save', 'not now'] };
3283
3315
  return { kind: v.kind, title: v.job ? v.job.what : '', detail: v.job ? v.job.why : '', buttons: [] };
3284
3316
  }
3285
3317
 
@@ -3299,7 +3331,9 @@ function createBetDesk(deps) {
3299
3331
  if (read && read.ok && (read.kind === 'answer' || read.kind === 'instruction')) return { act: read };
3300
3332
  const kind = read && read.ok ? read.kind : 'unclear';
3301
3333
  const reply = (read && read.ok && plainReply(read.reply)) || SAY_FALLBACK;
3302
- const suggestions = read && read.ok && reply !== SAY_FALLBACK ? plainSuggestions(read.suggestions) : [];
3334
+ const suggestions = read && read.ok && reply !== SAY_FALLBACK
3335
+ ? plainSuggestions((Array.isArray(read.suggestions) ? read.suggestions : []).map(you).filter(sug => !(card.buttons || []).some(b => sameLabel(b, sug))))
3336
+ : [];
3303
3337
  addTurns(t, { who: 'owner', text }, { who: 'orb', text: reply });
3304
3338
  return { stay: { ok: true, stay: true, kind: reply === SAY_FALLBACK ? 'unclear' : kind, reply, ...(suggestions.length ? { suggestions } : {}), thread: t.turns.slice() } };
3305
3339
  }
@@ -3349,6 +3383,35 @@ function createBetDesk(deps) {
3349
3383
  pump();
3350
3384
  return { ok: true, ack: plainReply(read.reply) || 'got it. the next idea will fit that.' };
3351
3385
  }
3386
+ // Words on the weekly number card are about that number, and the
3387
+ // reader says what they want. A plain number is this week's value. Stop
3388
+ // tracking this, or pick a different number, stops tracking it, and the
3389
+ // next card asks which number, if any. Ask me tomorrow is not now. Any
3390
+ // other answer or instruction gets a reply on the card and changes nothing.
3391
+ if (kind === 'report') {
3392
+ const stay = (reply) => {
3393
+ addTurns(t, { who: 'owner', text: said }, { who: 'orb', text: reply });
3394
+ return { ok: true, stay: true, kind: 'unclear', reply, thread: t.turns.slice() };
3395
+ };
3396
+ if (read.kind === 'answer') {
3397
+ const saved = report({ value: said });
3398
+ if (saved.ok) { endThread(); return saved; }
3399
+ return stay('type this week\'s number in the box, or say stop tracking this.');
3400
+ }
3401
+ if (read.intent === 'later') {
3402
+ passJob(cur);
3403
+ state.status = 'cleared';
3404
+ endThread();
3405
+ return { ok: true, ack: 'got it. i will ask tomorrow.' };
3406
+ }
3407
+ if (read.intent !== 'stop' && read.intent !== 'change') return stay(plainReply(read.reply) || 'this card keeps one number a week. say stop tracking this, or ask me tomorrow.');
3408
+ const name = cur.board.name;
3409
+ appendLedger(scoreboardFile, { workspace: deps.workspaceRoot, kind: 'clear', name, at: iso() });
3410
+ log({ card: 'bet', choice: 'scoreboard_clear', command: null, ran: 'in process', exit_code: 0 });
3411
+ state.status = 'cleared';
3412
+ endThread();
3413
+ return { ok: true, ack: `got it. i stopped tracking ${name}.` };
3414
+ }
3352
3415
  if (kind === 'yes' || kind === 'done') {
3353
3416
  const out = await steer({ member: cur.job.owner, text: said, confirm, note });
3354
3417
  if (out.ok) endThread();
@@ -3513,6 +3576,7 @@ module.exports = {
3513
3576
  FALLBACK_QUESTION,
3514
3577
  readScoreboard,
3515
3578
  readSaid,
3579
+ generateScoreboardChoices,
3516
3580
  scoreboardPath,
3517
3581
  choiceWhy,
3518
3582
  JOB_POLL_MS,
@@ -174,6 +174,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
174
174
  /* a chosen answer holds its spot with an amber edge, and the card takes no taps */
175
175
  .choice.chosen, .option.chosen, .field.chosen { border-color: var(--amber); background: rgba(255, 140, 66, 0.1); }
176
176
  .chosen .send { visibility: hidden; }
177
+ .chosen .send.reading { visibility: visible; }
178
+ .bet h1.said { font-size: 20px; line-height: 1.4; font-weight: 400; letter-spacing: 0; }
177
179
  .bet.holding { pointer-events: none; }
178
180
  .muted { opacity: 0.35; transition: opacity 0.2s ease; }
179
181
 
@@ -752,15 +754,18 @@ body.has-card .foot { position: static; margin-top: 28px; }
752
754
  b.addEventListener('click', function () {
753
755
  if (bet.busy) return;
754
756
  buzz();
755
- if (c.other) { typeHere(b, c.label, 60, function (text, field) { answer(field, { choice: 'other', text: text }); }); return; }
757
+ // typed words are read first, so a question back is explained and nothing is saved
758
+ if (c.other) { typeHere(b, c.label, 60, function (text, field) { say(field, { text: text }); }); return; }
756
759
  answer(b, { choice: c.id });
757
760
  });
761
+ if (c.other) b.classList.add('other');
758
762
  act.appendChild(b);
759
763
  });
760
764
  // a quiet line under the answers, empty until an answer does not go through
761
765
  act.appendChild(el('div', 'links'));
762
766
  box.appendChild(act);
763
- box.appendChild(sayBox());
767
+ // its own something else box is the one place to type
768
+ if (!q.choices.some(function (c) { return c.other; })) box.appendChild(sayBox());
764
769
  }
765
770
 
766
771
  // ---- answering in place: the eyes stay in one spot ----------------------------
@@ -887,10 +892,25 @@ body.has-card .foot { position: static; margin-top: 28px; }
887
892
  return spot.sendId;
888
893
  }
889
894
 
895
+ // while the words are read, one quiet ellipsis in the held spot: where
896
+ // send was in a box, or after a tapped reply. Nowhere else.
897
+ function reading(spot) {
898
+ var send = spot.querySelector('.send');
899
+ if (send) { send.textContent = '\u2026'; send.classList.add('reading'); return; }
900
+ spot.appendChild(el('span', 'reading', ' \u2026'));
901
+ }
902
+ function doneReading(spot) {
903
+ var send = spot.querySelector('.send.reading');
904
+ if (send) { send.textContent = 'send'; send.classList.remove('reading'); }
905
+ var dots = spot.querySelector('span.reading');
906
+ if (dots) dots.remove();
907
+ }
908
+
890
909
  function say(spot, body) {
891
910
  body.card = shownCard();
892
911
  body.send = sendId(spot, body.text);
893
912
  var heldAt = hold(spot);
913
+ reading(spot);
894
914
  api('/bet/say', body).catch(offline).then(function (r) {
895
915
  if (r.ok && !r.stay) { think(false, heldAt); return; }
896
916
  if (!r.ok && r.reason !== 'needs_confirm') { unsent(spot, r.message || 'that did not send. tap send to try again'); return; }
@@ -903,6 +923,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
903
923
  // the same tap) tries again. Left empty, the box turns back into the
904
924
  // button and the line goes with it.
905
925
  function unsent(spot, why) {
926
+ doneReading(spot);
906
927
  unhold();
907
928
  var note = spot.nextElementSibling;
908
929
  var input = spot.querySelector('input');
@@ -939,16 +960,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
939
960
  if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
940
961
  box.textContent = '';
941
962
  bet.drawn = null;
942
- box.appendChild(el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through')));
963
+ var title = el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through'));
964
+ box.appendChild(title);
965
+ // more than one line reads at the body size, in the same spot
966
+ var lines = document.createRange();
967
+ lines.selectNodeContents(title);
968
+ if (lines.getClientRects().length > 1) title.classList.add('said');
943
969
  if (own) box.appendChild(own);
944
970
  var act = el('div', 'act answers');
945
- if (r.stay) (r.suggestions || []).forEach(function (sug) { act.appendChild(sayChoice(sug, { text: sug })); });
971
+ var have = own ? Array.prototype.map.call(own.querySelectorAll('button'), function (b) { return b.textContent.trim().toLowerCase(); }) : [];
972
+ if (r.stay) (r.suggestions || []).forEach(function (sug) {
973
+ if (have.indexOf(String(sug).trim().toLowerCase()) < 0) act.appendChild(sayChoice(sug, { text: sug }));
974
+ });
946
975
  if (r.reason === 'needs_confirm') {
947
976
  act.appendChild(sayChoice('start it', { text: said, confirm: true }));
948
977
  act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
949
978
  }
950
979
  box.appendChild(act);
951
- box.appendChild(sayBox());
980
+ if (!own || !own.querySelector('.other')) box.appendChild(sayBox());
952
981
  }
953
982
 
954
983
  // the owner's own number, once a week: one box and one tap to save
@@ -978,7 +1007,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
978
1007
  input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
979
1008
  act.appendChild(input);
980
1009
  act.appendChild(save);
1010
+ // a quiet way out: moves on, saves nothing, and is not asked again today
1011
+ var links = el('div', 'links');
1012
+ var later = el('button', null, 'not now');
1013
+ later.type = 'button';
1014
+ later.addEventListener('click', function () { if (bet.busy) return; buzz(); think(false, 0); });
1015
+ links.appendChild(later);
1016
+ act.appendChild(links);
981
1017
  box.appendChild(act);
1018
+ box.appendChild(sayBox());
982
1019
  }
983
1020
 
984
1021
  // A conversation in one box. While the words are read the reply spot shows
package/lib/gm-phone.js CHANGED
@@ -509,6 +509,9 @@ function createPhoneHandler(deps) {
509
509
  async function betAction(p, body) {
510
510
  let out;
511
511
  if (p === '/bet/think') out = bet.next({ sharper: body.sharper === true });
512
+ // typed words in something else are read first, like say something: a
513
+ // question back or confusion is explained and nothing is saved
514
+ else if (p === '/bet/answer' && body.choice === 'other') out = await bet.say({ text: String(body.text || ''), card: String(body.card || '') });
512
515
  else if (p === '/bet/answer') out = bet.answer({ choice: String(body.choice || ''), text: String(body.text || ''), card: String(body.card || '') });
513
516
  else if (p === '/bet/report') out = bet.report({ value: body.value });
514
517
  else if (p === '/bet/pick') out = bet.pick({ id: String(body.id || '') });
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ // The person's own calendar day as YYYY-MM-DD. toISOString() gives the UTC
4
+ // day, which is already tomorrow in the evening west of Greenwich.
5
+ function localDay(date = new Date()) {
6
+ const d = date instanceof Date ? date : new Date(date);
7
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
8
+ }
9
+
10
+ module.exports = { localDay };
package/lib/manifest.js CHANGED
@@ -2,7 +2,6 @@ const crypto = require('crypto');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { stateHome } = require('./state-home');
5
- const { computeContentHash } = require('./journal');
6
5
 
7
6
  /**
8
7
  * Get the manifest file path for a business slug.
@@ -35,14 +34,6 @@ function saveManifest(slug, manifest) {
35
34
  fs.writeFileSync(p, JSON.stringify(manifest, null, 2));
36
35
  }
37
36
 
38
- /**
39
- * Compute SHA-256 hash of normalized content.
40
- * Delegates to journal.js computeContentHash.
41
- */
42
- function computeFileHash(content) {
43
- return computeContentHash(content);
44
- }
45
-
46
37
  /**
47
38
  * Build a manifest object from a set of files.
48
39
  * files: { [path]: { hash, size } }
@@ -247,7 +238,6 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
247
238
  module.exports = {
248
239
  loadManifest,
249
240
  saveManifest,
250
- computeFileHash,
251
241
  buildManifest,
252
242
  computeLocalHashes,
253
243
  threeWayCompare,
@@ -70,17 +70,6 @@ function asleepMembers(root = process.cwd()) {
70
70
  return Object.keys(members).filter((name) => normalizeMemberEntry(members[name]).awake === false).sort();
71
71
  }
72
72
 
73
- function isMemberLoopAwake(name, loopId, root = process.cwd()) {
74
- const entry = getMemberSwitch(name, root);
75
- if (entry.awake === false) return false;
76
- const id = String(loopId || '').trim();
77
- if (!id) return entry.awake !== false;
78
- if (Object.prototype.hasOwnProperty.call(entry.loops, id)) {
79
- return entry.loops[id] !== false;
80
- }
81
- return true;
82
- }
83
-
84
73
  function ensureMemberEntry(state, name) {
85
74
  if (!state.members[name] || typeof state.members[name] !== 'object') {
86
75
  state.members[name] = { awake: true, loops: {} };
@@ -25,10 +25,6 @@ function normalizeActor(value) {
25
25
  .replace(/^-+|-+$/g, '');
26
26
  }
27
27
 
28
- function envActor() {
29
- return normalizeActor(process.env.ATRIS_AGENT_ID || process.env.USER || '');
30
- }
31
-
32
28
  // Builder identity, oldest signal wins: explicit stamp, then who claimed it,
33
29
  // then whoever sent the first proof.
34
30
  function taskBuilder(task) {
package/lib/scorecard.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { parseTodo } = require('./todo');
4
+ const { localDay } = require('./local-day');
4
5
 
5
6
  const PRIVATE_MEMORY_ROOT = '.atris/presidio';
6
7
 
@@ -72,8 +73,8 @@ function listLogFiles(atrisDir, startDate, endDate = new Date()) {
72
73
  const logsDir = path.join(atrisDir, 'logs');
73
74
  if (!fs.existsSync(logsDir)) return [];
74
75
 
75
- const startKey = startDate.toISOString().slice(0, 10);
76
- const endKey = endDate.toISOString().slice(0, 10);
76
+ const startKey = localDay(startDate);
77
+ const endKey = localDay(endDate);
77
78
  const files = [];
78
79
 
79
80
  for (const year of fs.readdirSync(logsDir)) {
@@ -163,7 +164,7 @@ function countLessonsGenerated(atrisDir, pickedAt) {
163
164
  const lessonsPath = path.join(atrisDir, 'lessons.md');
164
165
  if (!fs.existsSync(lessonsPath)) return 0;
165
166
 
166
- const startDateKey = startAt.toISOString().slice(0, 10);
167
+ const startDateKey = localDay(startAt);
167
168
  return fs.readFileSync(lessonsPath, 'utf8')
168
169
  .split('\n')
169
170
  .reduce((count, line) => {
@@ -191,8 +192,8 @@ function buildScorecardData(atrisDir, { slug, pickedAt } = {}) {
191
192
 
192
193
  return {
193
194
  slug,
194
- startDate: startAt.toISOString().slice(0, 10),
195
- endDate: new Date().toISOString().slice(0, 10),
195
+ startDate: localDay(startAt),
196
+ endDate: localDay(),
196
197
  tasksShipped: Math.max(shipped, 0),
197
198
  tasksAttempted: Math.max(attempted, shipped),
198
199
  wallClockHours: Math.max(0, (Date.now() - startAt.getTime()) / (1000 * 60 * 60)),
@@ -221,7 +222,7 @@ function writeScorecard(atrisDir, data) {
221
222
  const {
222
223
  slug,
223
224
  startDate,
224
- endDate = new Date().toISOString().split('T')[0],
225
+ endDate = localDay(),
225
226
  tasksShipped = 0,
226
227
  tasksAttempted = 0,
227
228
  wallClockHours = 0,
package/lib/wiki.js CHANGED
@@ -31,8 +31,11 @@ function getWikiLinkRoot(mode = 'public') {
31
31
  return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
32
32
  }
33
33
 
34
+ // Local calendar day; toISOString is UTC and files an evening entry under
35
+ // tomorrow anywhere west of Greenwich, next to a local clock time.
34
36
  function today() {
35
- return new Date().toISOString().slice(0, 10);
37
+ const now = new Date();
38
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
36
39
  }
37
40
 
38
41
  function nowTime() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.74.0",
3
+ "version": "3.74.2",
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",