atris 3.74.1 → 3.74.3

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/atris/atris.md CHANGED
@@ -60,6 +60,15 @@ Labels used below:
60
60
  - `guarded`: checked by code or a pre-commit hook; bypassing is a bug
61
61
  - `expected`: convention; honor it or stop
62
62
 
63
+ ## values
64
+
65
+ Four values decide close calls. When a rule above does not settle it, these do.
66
+
67
+ - **Acceleration.** Progress compounds. Land small working changes early and often so the next agent builds on them today, not next week.
68
+ - **Accuracy.** A win counts only when it is checked. Prove it with a test that failed before and passes after, or a second builder's review when the stakes call for it. Faster tests make faster work.
69
+ - **Ambition.** Take one more level. When the small fix is done, look for the bigger version of the same win.
70
+ - **Agency.** Own the missing piece. A bug, a broken tool, or a gap in your way is yours to fix or file, not a reason to stop.
71
+
63
72
  ## taste
64
73
 
65
74
  What you ship should not read as generated. The test: if someone said "an AI made this," would they believe it instantly? If yes, that is the bug. The model has no words for restraint and it falls into gravity wells. Beat both.
package/atris.md CHANGED
@@ -82,6 +82,15 @@ Labels used below:
82
82
  - `guarded`: checked by code or a pre-commit hook; bypassing is a bug
83
83
  - `expected`: convention; honor it or stop
84
84
 
85
+ ## values
86
+
87
+ Four values decide close calls. When a rule above does not settle it, these do.
88
+
89
+ - **Acceleration.** Progress compounds. Land small working changes early and often so the next agent builds on them today, not next week.
90
+ - **Accuracy.** A win counts only when it is checked. Prove it with a test that failed before and passes after, or a second builder's review when the stakes call for it. Faster tests make faster work.
91
+ - **Ambition.** Take one more level. When the small fix is done, look for the bigger version of the same win.
92
+ - **Agency.** Own the missing piece. A bug, a broken tool, or a gap in your way is yours to fix or file, not a reason to stop.
93
+
85
94
  ## taste
86
95
 
87
96
  What you ship should not read as generated. The test: if someone said "an AI made this," would they believe it instantly? If yes, that is the bug. The model has no words for restraint and it falls into gravity wells. Beat both.
@@ -1157,16 +1157,19 @@ function lessonSlug(text, maxLen = 40) {
1157
1157
  return trimmed.replace(/-+$/g, '') || 'unknown';
1158
1158
  }
1159
1159
 
1160
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
1161
+ // already tomorrow in the evening anywhere west of Greenwich.
1162
+ function localDay(date = new Date()) {
1163
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
1164
+ }
1165
+
1160
1166
  /**
1161
1167
  * Write a lesson to atris/lessons.md
1162
1168
  * Appends a line in format: - **[YYYY-MM-DD] slug** - pass/fail - explanation
1163
1169
  */
1164
1170
  function writeLesson(cwd, slug, status, explanation) {
1165
1171
  const lessonsPath = path.join(cwd, 'atris', 'lessons.md');
1166
- // Local calendar day (YYYY-MM-DD); toISOString is UTC and dates an
1167
- // evening lesson tomorrow anywhere west of Greenwich.
1168
- const now = new Date();
1169
- const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
1172
+ const today = localDay();
1170
1173
  const lessonLine = `- **[${today}] ${slug}** - ${status} - ${explanation}`;
1171
1174
 
1172
1175
  if (!fs.existsSync(lessonsPath)) {
@@ -2962,7 +2965,7 @@ function markLessonAttempted(cwd, slug, reason) {
2962
2965
  if (!meta[slug] || typeof meta[slug] !== 'object') meta[slug] = {};
2963
2966
  meta[slug].status = 'attempted';
2964
2967
  meta[slug].attempts = (typeof meta[slug].attempts === 'number' ? meta[slug].attempts : 0) + 1;
2965
- meta[slug].last_attempt = new Date().toISOString().slice(0, 10);
2968
+ meta[slug].last_attempt = localDay();
2966
2969
  if (reason) meta[slug].last_attempt_reason = String(reason);
2967
2970
  try {
2968
2971
  const atrisDir = path.join(cwd, 'atris');
@@ -3184,7 +3187,7 @@ function hasRecentVerifyPrePass(cwd, taskTitle, windowDays = 7) {
3184
3187
  if (lessons.length === 0) return false;
3185
3188
  const cutoff = new Date();
3186
3189
  cutoff.setDate(cutoff.getDate() - windowDays);
3187
- const cutoffDate = cutoff.toISOString().split('T')[0];
3190
+ const cutoffDate = localDay(cutoff);
3188
3191
  const needle = `"${taskTitle}"`;
3189
3192
  for (const l of lessons) {
3190
3193
  if (l.id !== 'verify-not-falsifiable') continue;
@@ -1214,8 +1214,14 @@ async function startBusinessWorkspace(...args) {
1214
1214
  return state;
1215
1215
  }
1216
1216
 
1217
+ // Local calendar day; toISOString is UTC and dates evening work tomorrow
1218
+ // anywhere west of Greenwich.
1219
+ function localDay(now = new Date()) {
1220
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
1221
+ }
1222
+
1217
1223
  function defaultSharePath(cwd, role) {
1218
- const stamp = new Date().toISOString().slice(0, 10);
1224
+ const stamp = localDay();
1219
1225
  const roleSlug = slugifyName(role || 'collaborator');
1220
1226
  return path.join(cwd, 'atris', 'reports', `${stamp}-share-${roleSlug}.md`);
1221
1227
  }
@@ -1494,7 +1500,7 @@ async function onboardBusiness(...flags) {
1494
1500
  const intakeRel = path.relative(cwd, intakePath).replace(/\\/g, '/');
1495
1501
  const linksRel = path.relative(cwd, linksPath).replace(/\\/g, '/');
1496
1502
  const sourcesRel = path.relative(cwd, sourcesPath).replace(/\\/g, '/');
1497
- const today = new Date().toISOString().slice(0, 10);
1503
+ const today = localDay();
1498
1504
 
1499
1505
  const briefSlug = `${bizMeta.slug}-starter-brief`;
1500
1506
  const briefPath = path.join(cwd, 'atris', 'wiki', 'briefs', `${briefSlug}.md`);
@@ -2803,7 +2809,7 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
2803
2809
  description ? `\n> ${description}\n` : '',
2804
2810
  '\n## The Business\n\n[What problem does this solve?]\n',
2805
2811
  '## Revenue Model\n\n[How does this make money?]\n',
2806
- `---\n*Created: ${new Date().toISOString().split('T')[0]}*\n`,
2812
+ `---\n*Created: ${localDay()}*\n`,
2807
2813
  ].join(''));
2808
2814
  console.log(` Local scaffold: ${bizDir}/`);
2809
2815
  }
package/commands/clean.js CHANGED
@@ -3,6 +3,7 @@ const os = require('os');
3
3
  const path = require('path');
4
4
  const { spawnSync } = require('child_process');
5
5
  const escapeRegExp = require('../lib/escape-regexp');
6
+ const { localDay } = require('../lib/local-day');
6
7
  const { wikiMetabolismNudge } = require('../lib/wiki');
7
8
 
8
9
  /**
@@ -723,7 +724,7 @@ function checkPageStaleness(cwd, filePath) {
723
724
  page: filePath,
724
725
  staleSource: source,
725
726
  compiledDate: compiledMatch[1],
726
- sourceDate: changed.toISOString().split('T')[0]
727
+ sourceDate: localDay(changed)
727
728
  };
728
729
  }
729
730
  } catch {
package/commands/close.js CHANGED
@@ -85,8 +85,10 @@ function appendEvent(event, cwd = process.cwd()) {
85
85
  fs.appendFileSync(ledgerPath(cwd), `${JSON.stringify(event)}\n`);
86
86
  }
87
87
 
88
- function isoDateOnly(date) {
89
- return date.toISOString().slice(0, 10);
88
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
89
+ // already tomorrow in the evening anywhere west of Greenwich.
90
+ function localDay(date) {
91
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
90
92
  }
91
93
 
92
94
  function parseDate(value) {
@@ -156,7 +158,8 @@ function foldEvents(events, options = {}) {
156
158
  delete flag.parked_at;
157
159
  delete flag.why;
158
160
  } else if (event.kind === 'escalated') {
159
- const day = event.day || (event.at ? String(event.at).slice(0, 10) : null);
161
+ const at = event.at ? new Date(event.at) : null;
162
+ const day = event.day || (at && Number.isFinite(at.getTime()) ? localDay(at) : null);
160
163
  if (day && !flag.escalated_days.includes(day)) flag.escalated_days.push(day);
161
164
  } else if (event.kind === 'probed') {
162
165
  flag.probe_runs.push({
@@ -753,7 +756,7 @@ function commandSnooze(args, context = {}) {
753
756
  // Escalation core shared by the CLI verb and the hourly pulse tick: append
754
757
  // one escalated event per overdue flag per day, return the resulting state.
755
758
  function sweepState(cwd = process.cwd(), now = new Date(), options = {}) {
756
- const today = isoDateOnly(now);
759
+ const today = localDay(now);
757
760
  const autoClosed = autoCloseResolvedTaskReviewFlags(cwd, now);
758
761
  const autoParked = migrateOpenSlots(cwd, now, options);
759
762
  let flags = openFlags(cwd, { now });
@@ -600,7 +600,10 @@ function buildBenchmarkArtifact(name, packDir, options) {
600
600
  function recordExperimentRun(experimentId) {
601
601
  const root = process.cwd();
602
602
  const state = daily.readDailyState(root);
603
- const today = new Date().toISOString().slice(0, 10);
603
+ // Local calendar day; toISOString is UTC and records an evening run as
604
+ // tomorrow's anywhere west of Greenwich.
605
+ const now = new Date();
606
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
604
607
  state.last_run_date = today;
605
608
  if (!Array.isArray(state.history)) state.history = [];
606
609
  if (!state.history.includes(experimentId)) {
package/commands/gm.js CHANGED
@@ -287,6 +287,12 @@ function pickSeedPlayer(workspaceRoot, tasks, args = []) {
287
287
  return null;
288
288
  }
289
289
 
290
+ // Local calendar day; toISOString is UTC and stamps an evening mission
291
+ // with tomorrow anywhere west of Greenwich.
292
+ function localDay(now = new Date()) {
293
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
294
+ }
295
+
290
296
  function ensureStarterMission(taskDb, db, workspaceRoot, tasks, args = []) {
291
297
  if (hasFlagOrValue(args, '--no-seed')) return { tasks, seeded: null };
292
298
  if (!fs.existsSync(path.join(workspaceRoot, 'atris'))) return { tasks, seeded: null };
@@ -303,7 +309,7 @@ function ensureStarterMission(taskDb, db, workspaceRoot, tasks, args = []) {
303
309
  assigned_to: player,
304
310
  delegate_via: 'agentxp_gm',
305
311
  auto_seeded_by: 'atris gm',
306
- created_for_day: new Date().toISOString().slice(0, 10),
312
+ created_for_day: localDay(),
307
313
  },
308
314
  });
309
315
  taskDb.noteTask(db, {
@@ -1951,6 +1957,7 @@ async function gmCommand(...args) {
1951
1957
 
1952
1958
  module.exports = {
1953
1959
  displayCommand,
1960
+ ensureStarterMission,
1954
1961
  gmCommand,
1955
1962
  pickClub,
1956
1963
  playAction,
@@ -25,6 +25,7 @@ const path = require('path');
25
25
  const { spawnSync } = require('child_process');
26
26
  const gitSpawn = require('../lib/git-spawn');
27
27
  const ignoredCache = require('../lib/ignored-cache');
28
+ const { localDay } = require('../lib/local-day');
28
29
  const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
29
30
  const { loadCredentials } = require('../utils/auth');
30
31
  const pulse = require('../lib/pulse');
@@ -375,7 +376,7 @@ function agePhrase(ts, nowMs = Date.now()) {
375
376
  if (delta < 60 * 60 * 1000) return `${plural(Math.round(delta / (60 * 1000)), 'minute')} ago`;
376
377
  if (delta < DAY_MS) return `${plural(Math.round(delta / (60 * 60 * 1000)), 'hour')} ago`;
377
378
  if (delta < 30 * DAY_MS) return `${plural(Math.round(delta / DAY_MS), 'day')} ago`;
378
- return `on ${new Date(ms).toISOString().slice(0, 10)}`;
379
+ return `on ${localDay(new Date(ms))}`;
379
380
  }
380
381
 
381
382
  function plainSentence(value) {
@@ -1715,6 +1716,7 @@ async function run(argv = [], deps = {}) {
1715
1716
 
1716
1717
  module.exports = {
1717
1718
  run,
1719
+ agePhrase,
1718
1720
  runImprove,
1719
1721
  runImproveCore,
1720
1722
  parseImproveArgs,
@@ -27,9 +27,17 @@ function findWorkspaceRoot(start) {
27
27
  return null;
28
28
  }
29
29
 
30
+ // The local calendar day (YYYY-MM-DD), offsetDays back. toISOString() is the
31
+ // UTC day, which is already tomorrow in the evening west of Greenwich.
30
32
  function today(offsetDays = 0) {
31
- const d = new Date(Date.now() - offsetDays * 86400000);
32
- return d.toISOString().slice(0, 10);
33
+ const d = new Date();
34
+ d.setDate(d.getDate() - offsetDays);
35
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
36
+ }
37
+
38
+ function journalPath(ws, offsetDays = 0) {
39
+ const day = today(offsetDays);
40
+ return path.join(ws, 'atris', 'logs', day.slice(0, 4), `${day}.md`);
33
41
  }
34
42
 
35
43
  function existing(...candidates) {
@@ -178,8 +186,8 @@ async function interviewCommand(args = [], root = process.cwd(), { mode: forcedM
178
186
  if (subjectRaw === 'me' || subjectRaw === '') {
179
187
  mode = 'operator';
180
188
  contextPaths = existing(
181
- path.join(ws, 'atris', 'logs', '2026', `${today()}.md`),
182
- path.join(ws, 'atris', 'logs', '2026', `${today(1)}.md`),
189
+ journalPath(ws),
190
+ journalPath(ws, 1),
183
191
  path.join(ws, 'atris', 'now.md'),
184
192
  path.join(ws, 'atris', 'TODO.md')
185
193
  );
@@ -200,7 +208,7 @@ async function interviewCommand(args = [], root = process.cwd(), { mode: forcedM
200
208
  path.join(memberDir, 'now.md'),
201
209
  path.join(memberDir, 'logs'),
202
210
  path.join(memberDir, 'proofs'),
203
- path.join(ws, 'atris', 'logs', '2026', `${today()}.md`)
211
+ journalPath(ws)
204
212
  );
205
213
  } else {
206
214
  mode = 'expert';
package/commands/land.js CHANGED
@@ -473,7 +473,10 @@ function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
473
473
  }
474
474
 
475
475
  function salvageDir(root) {
476
- const stamp = new Date().toISOString().slice(0, 10);
476
+ // Local calendar day; toISOString is UTC and files an evening backup
477
+ // under tomorrow anywhere west of Greenwich.
478
+ const now = new Date();
479
+ const stamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
477
480
  const dir = path.join(root, '.atris', 'salvage', stamp);
478
481
  fs.mkdirSync(dir, { recursive: true });
479
482
  return dir;
@@ -1086,5 +1089,6 @@ module.exports = {
1086
1089
  landCommand,
1087
1090
  landSummary,
1088
1091
  reap,
1092
+ salvageDir,
1089
1093
  sharedBoard,
1090
1094
  };
package/commands/loops.js CHANGED
@@ -160,7 +160,7 @@ function setRegistryJob(id, enabled) {
160
160
  delete job.disabled_reason;
161
161
  } else {
162
162
  job.disabled = true;
163
- job.disabled_reason = `stopped via atris loops ${new Date().toISOString().slice(0, 10)}`;
163
+ job.disabled_reason = `stopped via atris loops ${todayIso()}`;
164
164
  }
165
165
  saveRegistry(registry);
166
166
  return true;
@@ -204,8 +204,11 @@ function controlLoop(id, action) {
204
204
  return 0;
205
205
  }
206
206
 
207
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
208
+ // already tomorrow in the evening anywhere west of Greenwich.
207
209
  function todayIso() {
208
- return new Date().toISOString().slice(0, 10);
210
+ const d = new Date();
211
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
209
212
  }
210
213
 
211
214
  function renderTemplate(text) {
@@ -3385,7 +3385,10 @@ function logDateForScan(relativePath, filePath) {
3385
3385
  const named = dateFromLogPath(relativePath);
3386
3386
  if (named) return named;
3387
3387
  try {
3388
- return fs.statSync(filePath).mtime.toISOString().slice(0, 10);
3388
+ // Local calendar day, like the dated journal names beside it;
3389
+ // toISOString is UTC and calls an evening write tomorrow's.
3390
+ const mtime = fs.statSync(filePath).mtime;
3391
+ return `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, '0')}-${String(mtime.getDate()).padStart(2, '0')}`;
3389
3392
  } catch {
3390
3393
  return null;
3391
3394
  }
@@ -12015,6 +12018,7 @@ async function memberCommand(subcommand, ...args) {
12015
12018
  }
12016
12019
 
12017
12020
  module.exports = {
12021
+ logDateForScan,
12018
12022
  memberRunDefaultWallSeconds,
12019
12023
  memberRunWantsReferee,
12020
12024
  proposalPromptForGoal,
package/commands/play.js CHANGED
@@ -6,6 +6,7 @@ const fs = require('fs');
6
6
  const { spawnSync } = require('child_process');
7
7
  const { getSessionProfile, loadCredentials } = require('../utils/auth');
8
8
  const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
9
+ const { localDay } = require('../lib/local-day');
9
10
 
10
11
  const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
11
12
  const AGENTXP_GLOBAL_SYNC_RULE = 'Run atris login, then sync. Owner-provided sync tokens are guided-demo fallback only.';
@@ -298,7 +299,7 @@ function ensureStarterMission(taskDb, db, workspaceRoot, player, tasks, args = [
298
299
  assigned_to: player,
299
300
  delegate_via: 'agentxp_play',
300
301
  auto_seeded_by: 'atris play',
301
- created_for_day: new Date().toISOString().slice(0, 10),
302
+ created_for_day: localDay(),
302
303
  },
303
304
  });
304
305
  taskDb.noteTask(db, {
@@ -487,4 +488,5 @@ async function playCommand(...args) {
487
488
 
488
489
  module.exports = {
489
490
  playCommand,
491
+ ensureStarterMission,
490
492
  };
package/commands/pull.js CHANGED
@@ -1023,7 +1023,9 @@ async function pullGeneralJournal(token, agentId) {
1023
1023
  for (let i = 0; i < 3; i++) {
1024
1024
  const d = new Date(today);
1025
1025
  d.setDate(d.getDate() - i);
1026
- dates.push(d.toISOString().split('T')[0]);
1026
+ // Local calendar day, the same day log sync files the journal under.
1027
+ // toISOString() is the UTC day, already tomorrow in the evening here.
1028
+ dates.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
1027
1029
  }
1028
1030
 
1029
1031
  let synced = 0;
@@ -1118,4 +1120,4 @@ async function pullMemberJournal(token, agentId, memberName, memberDir) {
1118
1120
  return synced;
1119
1121
  }
1120
1122
 
1121
- module.exports = { buildPullConflictReviewPacket, mergeSmartPullFiles, normalizePullFilePath, pullAtris };
1123
+ module.exports = { buildPullConflictReviewPacket, mergeSmartPullFiles, normalizePullFilePath, pullAtris, pullGeneralJournal };
@@ -227,7 +227,10 @@ function securityReviewCommand(argv = []) {
227
227
  // The landing: short, true, decision-ready. What you read after the overnight
228
228
  // loop, the opposite of a finding dump.
229
229
  function renderLanding(landing, threshold) {
230
- const date = new Date().toISOString().slice(0, 10);
230
+ // Local calendar day; toISOString is UTC and dates an evening run
231
+ // tomorrow anywhere west of Greenwich.
232
+ const now = new Date();
233
+ const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
231
234
  const L = ['', ` ✈ security landing, ${date}`, ''];
232
235
  if (landing.cleared) {
233
236
  L.push(` CLEARED TO SHIP no unresolved findings at the ${threshold.toUpperCase()} line`);
package/commands/soul.js CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const { localDay } = require('../lib/local-day');
15
16
 
16
17
  function findAtrisDir() {
17
18
  let dir = process.cwd();
@@ -272,7 +273,7 @@ function distillSoul(atrisDir) {
272
273
 
273
274
  // Append distilled section to persona
274
275
  const separator = '\n\n---\n\n';
275
- const distilledBlock = `<!-- Distilled ${new Date().toISOString().slice(0, 10)} | ${journalCount} journal entries, ${lessonItems.length} lessons, ${policyRules.length} policy rules -->\n\n` + distilled.join('\n');
276
+ const distilledBlock = `<!-- Distilled ${localDay()} | ${journalCount} journal entries, ${lessonItems.length} lessons, ${policyRules.length} policy rules -->\n\n` + distilled.join('\n');
276
277
 
277
278
  if (persona) {
278
279
  // Check if already has a distilled section, replace it
package/commands/sync.js CHANGED
@@ -43,6 +43,12 @@ function _walkTemplateDir(dir, base = dir) {
43
43
  return out;
44
44
  }
45
45
 
46
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
47
+ // already tomorrow in the evening anywhere west of Greenwich.
48
+ function localDay(date = new Date()) {
49
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
50
+ }
51
+
46
52
  /**
47
53
  * Substitute workspace metadata in template content.
48
54
  */
@@ -53,7 +59,7 @@ function _substituteParams(content, params) {
53
59
  .replace(/\{\{owner_email\}\}/g, params.owner_email || '')
54
60
  .replace(/\{\{business_id\}\}/g, params.business_id || '')
55
61
  .replace(/\{\{workspace_id\}\}/g, params.workspace_id || '')
56
- .replace(/\{\{today\}\}/g, params.today || new Date().toISOString().slice(0, 10))
62
+ .replace(/\{\{today\}\}/g, params.today || localDay())
57
63
  .replace(/\{\{workspace_template\}\}/g, params.workspace_template || 'business');
58
64
  }
59
65
 
@@ -337,7 +343,7 @@ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
337
343
  owner_email: bizMeta.owner_email || '',
338
344
  business_id: bizMeta.business_id || '',
339
345
  workspace_id: bizMeta.workspace_id || '',
340
- today: new Date().toISOString().slice(0, 10),
346
+ today: localDay(),
341
347
  workspace_template: template.key,
342
348
  };
343
349
  const force = options.force != null ? options.force : process.argv.includes('--force');
package/commands/task.js CHANGED
@@ -5857,7 +5857,7 @@ function delegateTask(args, options = {}) {
5857
5857
  assigned_to: owner,
5858
5858
  delegate_via: via,
5859
5859
  swarlo_channel: via === 'swarlo' ? String(tag || 'tasks') : null,
5860
- created_for_day: new Date().toISOString().slice(0, 10),
5860
+ created_for_day: localDay(),
5861
5861
  owner_resolution: ownerResolution.reason,
5862
5862
  };
5863
5863
  if (ownerResolution.requested_owner) metadata.requested_owner = ownerResolution.requested_owner;
@@ -6023,6 +6023,12 @@ function cmdKeep(args) {
6023
6023
  console.log(`put away ${count} ${noun} that were finished or sitting still.`);
6024
6024
  }
6025
6025
 
6026
+ // Local calendar day; toISOString is UTC and calls the evening tomorrow
6027
+ // anywhere west of Greenwich.
6028
+ function localDay(now = new Date()) {
6029
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
6030
+ }
6031
+
6026
6032
  function cmdDay(args) {
6027
6033
  const kept = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
6028
6034
  if (keptCount(kept)) autoRenderTodoFromDb(process.cwd());
@@ -6042,7 +6048,7 @@ function cmdDay(args) {
6042
6048
  failed: (projection.tasks || []).filter(task => task.status === 'failed').length,
6043
6049
  stale_failed: staleFailed.length,
6044
6050
  };
6045
- const date = new Date().toISOString().slice(0, 10);
6051
+ const date = localDay();
6046
6052
  if (wantsJson(args)) {
6047
6053
  printJson({
6048
6054
  ok: true,
package/commands/taste.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { addTaste, listTaste } = require('../lib/taste-lessons');
4
+ const { localDay } = require('../lib/local-day');
4
5
 
5
6
  const VALID_FLAGS = new Set(['--why', '--scope', '--example']);
6
7
 
@@ -86,7 +87,7 @@ function tasteCommand(args = [], options = {}) {
86
87
  why,
87
88
  scope,
88
89
  example,
89
- added: new Date().toISOString().slice(0, 10),
90
+ added: localDay(),
90
91
  root,
91
92
  });
92
93
  stdout.write(`saved the operator's ${entry.verdict} verdict for "${entry.subject}".\n`);
@@ -472,9 +472,11 @@ function dateStamp(now) {
472
472
  if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
473
473
  return now.slice(0, 10);
474
474
  }
475
- const value = now instanceof Date ? now : new Date(now || Date.now());
476
- if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
477
- return value.toISOString().slice(0, 10);
475
+ const parsed = now instanceof Date ? now : new Date(now || Date.now());
476
+ const value = Number.isNaN(parsed.getTime()) ? new Date() : parsed;
477
+ // Local calendar day. toISOString() is the UTC day, which is already
478
+ // tomorrow in the evening anywhere west of Greenwich.
479
+ return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
478
480
  }
479
481
 
480
482
  function fileXSearchBrief({ cwd, source, text, now } = {}) {
@@ -2530,9 +2530,12 @@ function searchApplyNow(now) {
2530
2530
  const value = now();
2531
2531
  if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) return value;
2532
2532
  const ms = Number(value);
2533
- if (Number.isFinite(ms) && ms > 0) return new Date(ms).toISOString().slice(0, 10);
2533
+ if (Number.isFinite(ms) && ms > 0) return dateStamp(ms);
2534
2534
  return undefined;
2535
2535
  }
2536
+ // The apply stub is dated by the local day, like the notes it follows;
2537
+ // left to itself the apply gate would stamp the UTC day.
2538
+ if (now === undefined || now === null || now instanceof Date || typeof now === 'number') return dateStamp(now);
2536
2539
  return now;
2537
2540
  }
2538
2541
 
@@ -3964,6 +3967,7 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
3964
3967
 
3965
3968
  module.exports = {
3966
3969
  DEFAULT_QUERY,
3970
+ searchApplyNow,
3967
3971
  DEFAULT_TIMEOUT_MS,
3968
3972
  parseYoutubeArgs,
3969
3973
  buildYoutubePayload,
package/commands/zen.js CHANGED
@@ -143,7 +143,7 @@ function printApply(result, out) {
143
143
  }
144
144
  line(dim(` ${teamLine(result.team_files)}`));
145
145
  if (result.saved.length) {
146
- line(dim(' everything is kept: git for-each-ref refs/archive refs/heads/zen-backup'));
146
+ line(dim(' nothing was thrown away. see what zen saved: git for-each-ref refs/archive refs/heads/zen-backup'));
147
147
  }
148
148
  line('');
149
149
  }
package/lib/apply-gate.js CHANGED
@@ -7,9 +7,11 @@ function dateStamp(now) {
7
7
  if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
8
8
  return now.slice(0, 10);
9
9
  }
10
- const value = now instanceof Date ? now : new Date(now || Date.now());
11
- if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
12
- return value.toISOString().slice(0, 10);
10
+ const parsed = now instanceof Date ? now : new Date(now || Date.now());
11
+ const value = Number.isNaN(parsed.getTime()) ? new Date() : parsed;
12
+ // Local calendar day. toISOString() is the UTC day, which is already
13
+ // tomorrow in the evening anywhere west of Greenwich.
14
+ return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
13
15
  }
14
16
 
15
17
  function parseApplyFields(text) {
@@ -134,7 +134,9 @@ function writeJsonAtomic(filePath, value) {
134
134
  function isoDate(now) {
135
135
  const date = now instanceof Date ? now : new Date(now || Date.now());
136
136
  if (Number.isNaN(date.getTime())) throw new AutoLessonsError('cannot file automatic lessons with an invalid date');
137
- return date.toISOString().slice(0, 10);
137
+ // Local calendar day; toISOString is UTC and dates an evening lesson
138
+ // tomorrow anywhere west of Greenwich.
139
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
138
140
  }
139
141
 
140
142
  function typedLesson(signal, options = {}) {
@@ -3,12 +3,13 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
+ const { localDay } = require('../local-day');
6
7
 
7
8
  const SCORECARD_SCHEMA = 'atris.experiment.daily.v1';
8
9
  const BENCH_SCHEMA = 'atris.bench.run.v1';
9
10
 
10
11
  function todayDate() {
11
- return new Date().toISOString().slice(0, 10);
12
+ return localDay();
12
13
  }
13
14
 
14
15
  function statePath(root) {
package/lib/gm-bet.js CHANGED
@@ -316,6 +316,7 @@ function buildQuestionPrompt(bundle, context, { answers = [], avoid = [] } = {})
316
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 "".`,
317
317
  `- choices: 2 or 3 short concrete answers he can tap, each under ${QUESTION_CHOICE_MAX} characters.`,
318
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".',
319
320
  '- a section marked stale is old: prefer the fresh ones, and never present a stale line as what is happening now.',
320
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.',
321
322
  '- plain lowercase-friendly words, no hype, no em dashes.',
@@ -346,6 +347,25 @@ function buildQuestionRewrite(previous, reasons) {
346
347
  ].join('\n');
347
348
  }
348
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
+
349
369
  // ---- reading free text: is it a question back, an answer, an instruction? ----
350
370
 
351
371
  const SAY_READ_MS = 15000;
@@ -383,9 +403,10 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
383
403
  '- instruction: he wants something done, or has a new idea. reply: one plain sentence saying what will happen next.',
384
404
  '- unclear: you cannot tell. reply: one short question back.',
385
405
  '- a message that ends with "?" or asks why, what, or how is a question back, never an answer.',
386
- '- 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.',
387
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.' : '',
388
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".',
389
410
  '',
390
411
  `## the card on screen (${card.kind})`,
391
412
  card.title || '',
@@ -424,6 +445,7 @@ async function generateScoreboardChoices({ workspaceRoot, bundle = null, deps =
424
445
  'Answer with strict JSON only, no prose and no code fence: {"choices": [{"name": "...", "up": true}]}',
425
446
  '- name: a short plain phrase under 40 characters. up: true when higher is better, false when lower is better.',
426
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".',
427
449
  '',
428
450
  "## this project's files",
429
451
  context || '(no readme or goals found)',
@@ -2708,7 +2730,7 @@ function createBetDesk(deps) {
2708
2730
  const board = readScoreboard(scoreboardFile, deps.workspaceRoot);
2709
2731
  if (!board) {
2710
2732
  if (state.passedQuestions.has('scoreboard')) return null;
2711
- const list = (state.suggestions || fallbackNumbers()).slice(0, 3);
2733
+ const list = (state.suggestions || fallbackNumbers()).slice(0, 3).map(c => ({ ...c, name: you(c.name) }));
2712
2734
  return {
2713
2735
  kind: 'question',
2714
2736
  id: 'question:scoreboard',
@@ -2812,7 +2834,7 @@ function createBetDesk(deps) {
2812
2834
  const row = readyQuestions().find(r => !state.passedQuestions.has(r.id));
2813
2835
  if (row) {
2814
2836
  const q = row.question;
2815
- 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) })) } };
2816
2838
  }
2817
2839
  const F = FALLBACK_QUESTION;
2818
2840
  const said = readFocus(focusFile, deps.workspaceRoot)[F.key];
@@ -3266,6 +3288,9 @@ function createBetDesk(deps) {
3266
3288
  t.turns = [...t.turns, ...turns].slice(-THREAD_TURNS);
3267
3289
  }
3268
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();
3269
3294
  const plainSuggestions = list => (Array.isArray(list) ? list : [])
3270
3295
  .filter(x => typeof x === 'string' && x.trim() && x.length <= 40 && !ownerWord(x) && !PROCESS_TALK.test(x))
3271
3296
  .slice(0, 3);
@@ -3306,7 +3331,9 @@ function createBetDesk(deps) {
3306
3331
  if (read && read.ok && (read.kind === 'answer' || read.kind === 'instruction')) return { act: read };
3307
3332
  const kind = read && read.ok ? read.kind : 'unclear';
3308
3333
  const reply = (read && read.ok && plainReply(read.reply)) || SAY_FALLBACK;
3309
- 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
+ : [];
3310
3337
  addTurns(t, { who: 'owner', text }, { who: 'orb', text: reply });
3311
3338
  return { stay: { ok: true, stay: true, kind: reply === SAY_FALLBACK ? 'unclear' : kind, reply, ...(suggestions.length ? { suggestions } : {}), thread: t.turns.slice() } };
3312
3339
  }
@@ -3549,6 +3576,7 @@ module.exports = {
3549
3576
  FALLBACK_QUESTION,
3550
3577
  readScoreboard,
3551
3578
  readSaid,
3579
+ generateScoreboardChoices,
3552
3580
  scoreboardPath,
3553
3581
  choiceWhy,
3554
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
 
@@ -890,10 +892,25 @@ body.has-card .foot { position: static; margin-top: 28px; }
890
892
  return spot.sendId;
891
893
  }
892
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
+
893
909
  function say(spot, body) {
894
910
  body.card = shownCard();
895
911
  body.send = sendId(spot, body.text);
896
912
  var heldAt = hold(spot);
913
+ reading(spot);
897
914
  api('/bet/say', body).catch(offline).then(function (r) {
898
915
  if (r.ok && !r.stay) { think(false, heldAt); return; }
899
916
  if (!r.ok && r.reason !== 'needs_confirm') { unsent(spot, r.message || 'that did not send. tap send to try again'); return; }
@@ -906,6 +923,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
906
923
  // the same tap) tries again. Left empty, the box turns back into the
907
924
  // button and the line goes with it.
908
925
  function unsent(spot, why) {
926
+ doneReading(spot);
909
927
  unhold();
910
928
  var note = spot.nextElementSibling;
911
929
  var input = spot.querySelector('input');
@@ -942,10 +960,18 @@ body.has-card .foot { position: static; margin-top: 28px; }
942
960
  if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
943
961
  box.textContent = '';
944
962
  bet.drawn = null;
945
- 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');
946
969
  if (own) box.appendChild(own);
947
970
  var act = el('div', 'act answers');
948
- 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
+ });
949
975
  if (r.reason === 'needs_confirm') {
950
976
  act.appendChild(sayChoice('start it', { text: said, confirm: true }));
951
977
  act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
@@ -35,13 +35,22 @@ function commitsSinceMidnight(root) {
35
35
  }
36
36
  }
37
37
 
38
+ // Local calendar day (YYYY-MM-DD) of a time, or null. Close events are stored
39
+ // as UTC ISO times, and the UTC day is already tomorrow in the evening west
40
+ // of Greenwich, so "today" is compared as local days on both sides.
41
+ function localDay(value) {
42
+ const d = value instanceof Date ? value : new Date(value || NaN);
43
+ if (Number.isNaN(d.getTime())) return null;
44
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
45
+ }
46
+
38
47
  function closedToday(cwd, now) {
39
48
  try {
40
49
  const close = require('../commands/close');
41
- const today = now.toISOString().slice(0, 10);
50
+ const today = localDay(now);
42
51
  return close.foldEvents(close.readEvents(cwd), { now })
43
- .filter((f) => (f.status === 'closed' && String(f.closed_at || '').startsWith(today))
44
- || (f.status === 'dissolved' && String(f.dissolved_at || '').startsWith(today)))
52
+ .filter((f) => (f.status === 'closed' && localDay(f.closed_at) === today)
53
+ || (f.status === 'dissolved' && localDay(f.dissolved_at) === today))
45
54
  .map((f) => ({ what: f.what, status: f.status }));
46
55
  } catch {
47
56
  return [];
@@ -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 };
@@ -115,6 +115,13 @@ function pairOrbRuns(records) {
115
115
  return { runs, orphans };
116
116
  }
117
117
 
118
+ // Local calendar day (YYYY-MM-DD). Orb picks are dated by the day they were
119
+ // made; toISOString() is the UTC day, already tomorrow in the evening west of
120
+ // Greenwich.
121
+ function localDay(date) {
122
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
123
+ }
124
+
118
125
  function readOrbScorecard(root, { days = DEFAULT_DAYS, now = Date.now() } = {}) {
119
126
  const windowDays = Number(days);
120
127
  if (!Number.isInteger(windowDays) || windowDays < 1) {
@@ -125,8 +132,8 @@ function readOrbScorecard(root, { days = DEFAULT_DAYS, now = Date.now() } = {})
125
132
 
126
133
  const scorecard = emptyOrbScorecard(windowDays);
127
134
  const cutoffMs = nowMs - (windowDays * DAY_MS);
128
- const cutoffDay = new Date(cutoffMs).toISOString().slice(0, 10);
129
- const today = new Date(nowMs).toISOString().slice(0, 10);
135
+ const cutoffDay = localDay(new Date(cutoffMs));
136
+ const today = localDay(new Date(nowMs));
130
137
  scorecard.picks = readOrbPicks(root, cutoffDay, today);
131
138
 
132
139
  const indexPath = path.join(root, '.atris', 'state', 'orb-runs', 'index.jsonl');
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.1",
3
+ "version": "3.74.3",
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",