atris 3.73.1 → 3.74.1

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 (51) 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 +4 -8
  6. package/commands/avail.js +36 -7
  7. package/commands/computer.js +3 -1
  8. package/commands/console.js +1 -1
  9. package/commands/context-sync.js +1 -60
  10. package/commands/fleet.js +0 -1
  11. package/commands/lifecycle.js +17 -6
  12. package/commands/log-sync.js +1 -1
  13. package/commands/member.js +158 -18
  14. package/commands/mission.js +157 -69
  15. package/commands/now.js +0 -7
  16. package/commands/plugin.js +1 -1
  17. package/commands/pull.js +1 -1
  18. package/commands/push.js +0 -1
  19. package/commands/radar.js +0 -4
  20. package/commands/recap.js +25 -1
  21. package/commands/report.js +0 -4
  22. package/commands/review.js +1 -1
  23. package/commands/setup.js +6 -2
  24. package/commands/slop.js +8 -2
  25. package/commands/task.js +0 -522
  26. package/commands/terminal.js +1 -1
  27. package/commands/youtube.js +4 -6
  28. package/commands/zen.js +4 -4
  29. package/lib/agent-today.js +412 -0
  30. package/lib/autoland.js +0 -8
  31. package/lib/bench/runner.js +0 -4
  32. package/lib/chat-log-scan.js +0 -28
  33. package/lib/cli-scope.js +4 -2
  34. package/lib/engine-registry.js +0 -4
  35. package/lib/falsifier-probe.js +2 -1
  36. package/lib/file-ops.js +6 -1
  37. package/lib/first-minute.js +2 -0
  38. package/lib/gm-bet.js +125 -30
  39. package/lib/gm-phone-page.js +398 -99
  40. package/lib/gm-phone.js +30 -3
  41. package/lib/handoff-wake.js +180 -0
  42. package/lib/manifest.js +0 -10
  43. package/lib/member-switches.js +0 -11
  44. package/lib/member-up.js +18 -3
  45. package/lib/mission-protected-lane.js +38 -6
  46. package/lib/review-integrity.js +0 -4
  47. package/lib/spawn-env.js +92 -3
  48. package/lib/task-proof.js +2 -1
  49. package/lib/task-receipt.js +3 -2
  50. package/lib/yes-lock.js +633 -81
  51. package/package.json +1 -1
@@ -38,6 +38,7 @@ const {
38
38
  resolveEngineForRoleWithPreference,
39
39
  engineFailureHealthStatus,
40
40
  engineStallReason,
41
+ engineRegistryView,
41
42
  recordEngineRunHealth,
42
43
  setEngineHealth,
43
44
  } = require('../lib/engine-registry');
@@ -1905,15 +1906,58 @@ function recordMemberHandoff(fromMember, toMember, sliceSummary, root = process.
1905
1906
  next_command: `atris member go ${to}`,
1906
1907
  created_at: now,
1907
1908
  };
1909
+ const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
1910
+ try {
1911
+ withHandoffsLock(handoffsFile, () => fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8'));
1912
+ } catch {
1913
+ return null;
1914
+ }
1908
1915
  try {
1909
- const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
1910
- fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
1911
- fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8');
1912
1916
  autoQueueMemberNextPush(to, `${from} handoff: ${cleanSummary}`, `Baton pass from ${from}`, parentRoot);
1913
1917
  } catch {}
1914
1918
  return handoff;
1915
1919
  }
1916
1920
 
1921
+ // Every write to member-handoffs.jsonl (append or whole-file rewrite) holds
1922
+ // one lock file beside it, so concurrent handoffs never lose a row.
1923
+ // (Codex review round 3 on #1483.)
1924
+ function withHandoffsLock(handoffsFile, fn) {
1925
+ fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
1926
+ const lockFile = path.join(path.dirname(handoffsFile), 'member-handoffs.lock');
1927
+ return require('../lib/handoff-wake').withFileLock(lockFile, fn, { staleMs: 60 * 1000, waitMs: 30 * 1000 });
1928
+ }
1929
+
1930
+ function rewriteHandoffs(handoffsFile, lines) {
1931
+ const tmp = `${handoffsFile}.${process.pid}.tmp`;
1932
+ fs.writeFileSync(tmp, `${lines.join('\n')}\n`, 'utf8');
1933
+ fs.renameSync(tmp, handoffsFile);
1934
+ }
1935
+
1936
+ // Merge `patch` into the newest handoff row with this id. Returns the
1937
+ // updated row, or null when it is not on file.
1938
+ function updateMemberHandoff(handoffId, patch, root = process.cwd()) {
1939
+ const id = String(handoffId || '');
1940
+ if (!id || !patch) return null;
1941
+ const handoffsFile = path.join(resolveMemberWorkspaceRoot(root), '.atris', 'state', 'member-handoffs.jsonl');
1942
+ try {
1943
+ return withHandoffsLock(handoffsFile, () => {
1944
+ const lines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
1945
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
1946
+ let row = null;
1947
+ try { row = JSON.parse(lines[i]); } catch {}
1948
+ if (!row || row.id !== id) continue;
1949
+ const updated = { ...row, ...patch };
1950
+ lines[i] = JSON.stringify(updated);
1951
+ rewriteHandoffs(handoffsFile, lines);
1952
+ return updated;
1953
+ }
1954
+ return null;
1955
+ });
1956
+ } catch {
1957
+ return null;
1958
+ }
1959
+ }
1960
+
1917
1961
  function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
1918
1962
  const receiver = String(receiverMember || '').trim().toLowerCase();
1919
1963
  if (!receiver) return 0;
@@ -1921,28 +1965,30 @@ function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
1921
1965
  const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
1922
1966
  if (!fs.existsSync(handoffsFile)) return 0;
1923
1967
  try {
1924
- const rawLines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
1925
- let closedCount = 0;
1926
- const now = stampIso();
1927
- const updatedLines = rawLines.map((line) => {
1928
- try {
1929
- const row = JSON.parse(line);
1930
- if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
1931
- closedCount++;
1932
- return JSON.stringify({ ...row, status: 'closed', closed_at: now });
1933
- }
1934
- } catch {}
1935
- return line;
1936
- });
1937
- if (closedCount > 0) {
1938
- fs.writeFileSync(handoffsFile, `${updatedLines.join('\n')}\n`, 'utf8');
1939
- }
1940
- return closedCount;
1968
+ return withHandoffsLock(handoffsFile, () => closeHandoffRows(handoffsFile, receiver));
1941
1969
  } catch {
1942
1970
  return 0;
1943
1971
  }
1944
1972
  }
1945
1973
 
1974
+ function closeHandoffRows(handoffsFile, receiver) {
1975
+ const rawLines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
1976
+ let closedCount = 0;
1977
+ const now = stampIso();
1978
+ const updatedLines = rawLines.map((line) => {
1979
+ try {
1980
+ const row = JSON.parse(line);
1981
+ if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
1982
+ closedCount++;
1983
+ return JSON.stringify({ ...row, status: 'closed', closed_at: now });
1984
+ }
1985
+ } catch {}
1986
+ return line;
1987
+ });
1988
+ if (closedCount > 0) rewriteHandoffs(handoffsFile, updatedLines);
1989
+ return closedCount;
1990
+ }
1991
+
1946
1992
  function memberMissionFile(owner, root = process.cwd()) {
1947
1993
  const dir = memberDir(owner, root);
1948
1994
  if (!dir) return null;
@@ -3217,6 +3263,7 @@ function missionFromArgs(args) {
3217
3263
  '--task',
3218
3264
  '--ask',
3219
3265
  '--yes-lock',
3266
+ '--yes-lock-parent',
3220
3267
  '--campaign',
3221
3268
  '--reserve',
3222
3269
  '--mission-id',
@@ -3260,7 +3307,18 @@ function missionFromArgs(args) {
3260
3307
  const taskIds = readRepeatedFlag(args, '--task');
3261
3308
  // MEMBER.md approval-required becomes an open human ask the owner must clear.
3262
3309
  const humanAsks = [...readRepeatedFlag(args, '--ask'), ...memberApprovalAsks(owner, process.cwd(), objective)];
3263
- const yesLocks = readYesLockSeal(readFlag(args, '--yes-lock', ''), wantsJson(args));
3310
+ // The seal is never read from text a caller passes through: two
3311
+ // --yes-lock words (one could be the objective) refuse the start, and a
3312
+ // run woken by a handoff carries its seal in the environment, which every
3313
+ // job it starts inherits. (Codex review round 6 on #1483.)
3314
+ if (args.filter(isYesLockWord).length > 1) {
3315
+ exitMissionError('mission start got more than one --yes-lock, so it is not clear which seal applies; nothing started', 2, wantsJson(args));
3316
+ }
3317
+ const handoffSeal = String(process.env.ATRIS_HANDOFF_YES_LOCK || '').trim();
3318
+ const yesLocks = readYesLockSeal(handoffSeal || readFlag(args, '--yes-lock', ''), wantsJson(args));
3319
+ const yesLockParent = yesLocks.length
3320
+ ? String((handoffSeal ? process.env.ATRIS_HANDOFF_YES_LOCK_PARENT : readFlag(args, '--yes-lock-parent', '')) || '').trim()
3321
+ : '';
3264
3322
  const drive = missionDrive(readFlag(args, '--destination', ''));
3265
3323
  const alwaysOn = hasFlag(args, '--always-on');
3266
3324
  const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
@@ -3299,6 +3357,9 @@ function missionFromArgs(args) {
3299
3357
  human_asks: humanAsks,
3300
3358
  // the sealed owner steps: the guard holds what each locks until the owner says yes
3301
3359
  ...(yesLocks.length ? { yes_locks: { steps: yesLocks, sealed_at: stampIso() } } : {}),
3360
+ // a run woken by a handoff: the job it came from, only to show its yes
3361
+ // cards there (lib/yes-lock.js handoffYesCards)
3362
+ ...(yesLockParent ? { yes_lock_parent: yesLockParent } : {}),
3302
3363
  ...(campaignId ? { campaign_id: campaignId } : {}),
3303
3364
  ...(drive ? { drive } : {}),
3304
3365
  next_action: verifier ? 'run verifier with `atris mission tick <id> --verify`' : 'define verifier or run next task',
@@ -3319,7 +3380,12 @@ function missionFromArgs(args) {
3319
3380
 
3320
3381
  // --yes-lock '[{"step":3,"text":"...","categories":["publish"]}]': the owner
3321
3382
  // steps a job may not do before the owner says yes. Bad input stops the start.
3322
- const YES_LOCK_CATEGORIES = new Set(['publish', 'tag', 'post', 'send', 'merge', 'deploy']);
3383
+ const YES_LOCK_CATEGORIES = new Set(['publish', 'tag', 'post', 'send', 'merge', 'deploy', 'spend']);
3384
+ function isYesLockWord(arg) {
3385
+ const text = String(arg || '').trim();
3386
+ return text === '--yes-lock' || text.startsWith('--yes-lock=');
3387
+ }
3388
+
3323
3389
  function readYesLockSeal(raw, asJson) {
3324
3390
  if (!raw) return [];
3325
3391
  let list;
@@ -3329,7 +3395,7 @@ function readYesLockSeal(raw, asJson) {
3329
3395
  exitMissionError('--yes-lock takes a JSON list of {step, text, categories}', 2, asJson);
3330
3396
  }
3331
3397
  const ok = Array.isArray(list) && list.every(l => l && Number.isInteger(l.step) && l.step > 0 && Array.isArray(l.categories) && l.categories.length && l.categories.every(c => YES_LOCK_CATEGORIES.has(c)));
3332
- if (!ok) exitMissionError('--yes-lock takes a JSON list of {step, text, categories}, with categories from publish, tag, post, send, merge, deploy', 2, asJson);
3398
+ if (!ok) exitMissionError('--yes-lock takes a JSON list of {step, text, categories}, with categories from publish, tag, post, send, merge, deploy, spend', 2, asJson);
3333
3399
  return list.map(l => ({ step: l.step, text: String(l.text || '').slice(0, 200), categories: [...new Set(l.categories)] }));
3334
3400
  }
3335
3401
 
@@ -3797,10 +3863,6 @@ function chooseNextMissionAnalysis(mission, root = process.cwd()) {
3797
3863
  return { target: null, near_misses: [] };
3798
3864
  }
3799
3865
 
3800
- function chooseNextMissionTarget(mission, root = process.cwd()) {
3801
- return chooseNextMissionAnalysis(mission, root).target;
3802
- }
3803
-
3804
3866
  function normalizeMissionOwner(value) {
3805
3867
  return String(value || '').trim().toLowerCase();
3806
3868
  }
@@ -4512,6 +4574,15 @@ function normalizedObjective(text) {
4512
4574
  return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
4513
4575
  }
4514
4576
 
4577
+ // True when `existing` runs under the lock `wanted` carries: the start
4578
+ // carries none, or the same seal.
4579
+ function lockCovered(existing, wanted) {
4580
+ const want = (wanted && wanted.yes_locks && wanted.yes_locks.steps) || [];
4581
+ if (!want.length) return true;
4582
+ const have = (existing && existing.yes_locks && existing.yes_locks.steps) || [];
4583
+ return JSON.stringify(have) === JSON.stringify(want);
4584
+ }
4585
+
4515
4586
  function findActiveTwinMission(objective, owner, root = process.cwd()) {
4516
4587
  const wantObjective = normalizedObjective(objective);
4517
4588
  const wantOwner = String(owner || '').trim().toLowerCase();
@@ -4547,6 +4618,11 @@ function startMission(args, options = {}) {
4547
4618
  const idLikeObjective = String(mission.objective || '').trim();
4548
4619
  if (/^mission-\d{4}-\d{2}-\d{2}-/.test(idLikeObjective)) {
4549
4620
  const existing = resolveMission(idLikeObjective);
4621
+ if (existing && !lockCovered(existing, mission)) {
4622
+ // A start carrying a yes-lock never resumes a job under a weaker lock:
4623
+ // it would run without it. (Codex review round 4 on #1483.)
4624
+ exitMissionError(`${existing.id} is not under the yes-lock this start carries, so resuming it would run without the yes-lock; start a new job instead`, 2, asJson);
4625
+ }
4550
4626
  if (existing) {
4551
4627
  printJsonOrText(
4552
4628
  { ok: true, action: 'mission_recovered', recovered: true, mission: existing, note: 'objective looked like a mission id; recovered the existing mission instead of creating a new one' },
@@ -4560,55 +4636,57 @@ function startMission(args, options = {}) {
4560
4636
  }
4561
4637
  exitMissingMission(idLikeObjective, 1, asJson);
4562
4638
  }
4563
- if (!hasFlag(args, '--duplicate')) {
4564
- const twin = findActiveTwinMission(mission.objective, mission.owner);
4565
- if (twin) {
4566
- const requestedDestination = mission.drive?.destination || '';
4567
- if (requestedDestination && twin.drive?.destination && requestedDestination !== twin.drive.destination) {
4568
- const proposed = proposeMissionDestinationChange(twin, requestedDestination);
4569
- const proposal = proposed.drive.pending_destination_proposal;
4570
- const missionRoot = twin.worktree_root || process.cwd();
4571
- const { mission: saved } = saveMission(
4572
- proposed,
4573
- missionRoot,
4574
- 'mission_destination_change_proposed',
4575
- {
4576
- current_destination: twin.drive.destination,
4577
- proposed_destination: proposal.destination,
4578
- reason: proposal.reason,
4579
- },
4580
- );
4581
- printJsonOrText(
4582
- {
4583
- ok: true,
4584
- action: 'mission_destination_change_proposed',
4585
- reused: true,
4586
- operator_gate_required: true,
4587
- mission: saved,
4588
- proposal,
4589
- },
4590
- [
4591
- `Destination change proposed for ${saved.id}.`,
4592
- `Current destination: ${saved.drive.destination}`,
4593
- `Proposed destination: ${proposal.destination}`,
4594
- `Operator gate: ${proposal.reason}.`,
4595
- ],
4596
- asJson,
4597
- );
4598
- return;
4599
- }
4639
+ // A start that carries a yes-lock never reuses a twin under a different
4640
+ // lock (or none): the reused job would run without it. It gets a fresh
4641
+ // job under the lock instead. (Codex review rounds 3 and 4 on #1483.)
4642
+ const twinCandidate = hasFlag(args, '--duplicate') ? null : findActiveTwinMission(mission.objective, mission.owner);
4643
+ if (twinCandidate && lockCovered(twinCandidate, mission)) {
4644
+ const twin = twinCandidate;
4645
+ const requestedDestination = mission.drive?.destination || '';
4646
+ if (requestedDestination && twin.drive?.destination && requestedDestination !== twin.drive.destination) {
4647
+ const proposed = proposeMissionDestinationChange(twin, requestedDestination);
4648
+ const proposal = proposed.drive.pending_destination_proposal;
4649
+ const missionRoot = twin.worktree_root || process.cwd();
4650
+ const { mission: saved } = saveMission(
4651
+ proposed,
4652
+ missionRoot,
4653
+ 'mission_destination_change_proposed',
4654
+ {
4655
+ current_destination: twin.drive.destination,
4656
+ proposed_destination: proposal.destination,
4657
+ reason: proposal.reason,
4658
+ },
4659
+ );
4600
4660
  printJsonOrText(
4601
- { ok: true, action: 'mission_reused', reused: true, mission: twin, note: 'an active mission with this objective and owner already exists; resumed instead of cloning (pass --duplicate to force a second one)' },
4661
+ {
4662
+ ok: true,
4663
+ action: 'mission_destination_change_proposed',
4664
+ reused: true,
4665
+ operator_gate_required: true,
4666
+ mission: saved,
4667
+ proposal,
4668
+ },
4602
4669
  [
4603
- `Already active: ${twin.id} (${twin.status})`,
4604
- `Same objective, same owner, reusing it instead of starting a clone.`,
4605
- `Resume: atris mission run ${twin.id}`,
4606
- `Really want a second one: re-run with --duplicate`,
4670
+ `Destination change proposed for ${saved.id}.`,
4671
+ `Current destination: ${saved.drive.destination}`,
4672
+ `Proposed destination: ${proposal.destination}`,
4673
+ `Operator gate: ${proposal.reason}.`,
4607
4674
  ],
4608
4675
  asJson,
4609
4676
  );
4610
4677
  return;
4611
4678
  }
4679
+ printJsonOrText(
4680
+ { ok: true, action: 'mission_reused', reused: true, mission: twin, note: 'an active mission with this objective and owner already exists; resumed instead of cloning (pass --duplicate to force a second one)' },
4681
+ [
4682
+ `Already active: ${twin.id} (${twin.status})`,
4683
+ `Same objective, same owner, reusing it instead of starting a clone.`,
4684
+ `Resume: atris mission run ${twin.id}`,
4685
+ `Really want a second one: re-run with --duplicate`,
4686
+ ],
4687
+ asJson,
4688
+ );
4689
+ return;
4612
4690
  }
4613
4691
  // Intake gate: a mission without a verifier is a planning wish, and planning
4614
4692
  // wishes silt up the queue (11 swept on 2026-07-06 with zero ticks). Refuse
@@ -4736,6 +4814,13 @@ function startMission(args, options = {}) {
4736
4814
  lane: saved.lane,
4737
4815
  verifier: saved.verifier,
4738
4816
  });
4817
+ if (saved.yes_lock_parent && saved.yes_locks && saved.yes_locks.steps) {
4818
+ // the lock record names the job it came from, before any command is held
4819
+ try {
4820
+ require('../lib/yes-lock').writeYesLocks(path.join(require('../lib/state-home').atrisHome(), 'state'), saved.id,
4821
+ { seal: saved.yes_locks.steps, parentMissionId: saved.yes_lock_parent });
4822
+ } catch {}
4823
+ }
4739
4824
  const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd(), codeBaseline);
4740
4825
  const nextTickCommand = `atris mission tick ${saved.id}${saved.verifier ? ' --verify' : ''}`;
4741
4826
  const payload = {
@@ -12807,6 +12892,9 @@ module.exports = {
12807
12892
  usefulClaudeReceiptSummary,
12808
12893
  autoQueueMemberNextPush,
12809
12894
  recordMemberHandoff,
12895
+ updateMemberHandoff,
12896
+ resolveMemberWorkspaceRoot,
12897
+ isYesLockWord,
12810
12898
  closeMemberHandoffsForReceiver,
12811
12899
  cappedClaudeReceiptText,
12812
12900
  extractLayerFromReceiptText,
package/commands/now.js CHANGED
@@ -273,13 +273,6 @@ function taskReceiptTitle(row) {
273
273
  return compactLine(row?.state?.title || row?.title || row?.task_id || 'untitled work');
274
274
  }
275
275
 
276
- // The receipts behind "Completed receipts today", as lines a human can read:
277
- // one per receipt, landed result + human proof state. Shares the collector
278
- // with the count so the number and the visible list always match.
279
- function todayTaskReceiptLines(root = process.cwd(), date = new Date(), limit = 6) {
280
- return formatTaskReceiptLines(collectTaskReceiptsToday(root, date), limit);
281
- }
282
-
283
276
  // Format an already-collected receipt list, so a caller that needs both the
284
277
  // count and the lines (renderDefaultNow) parses the receipt files only once.
285
278
  function formatTaskReceiptLines(receipts, limit = 6) {
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
- const { findAllSkills, parseFrontmatter } = require('./skill');
5
+ const { findAllSkills } = require('./skill');
6
6
 
7
7
  // --- Recursive Copy Helper ---
8
8
 
package/commands/pull.js CHANGED
@@ -7,7 +7,7 @@ const { loadConfig } = require('../utils/config');
7
7
  const { getLogPath } = require('../lib/file-ops');
8
8
  const { parseJournalSections, mergeSections, reconstructJournal } = require('../lib/journal');
9
9
  const { loadBusinesses, businessMatchesSlug } = require('./business');
10
- const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare, isIgnoredSyncPath } = require('../lib/manifest');
10
+ const { loadManifest, saveManifest, buildManifest, computeLocalHashes, threeWayCompare, isIgnoredSyncPath } = require('../lib/manifest');
11
11
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
12
12
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
13
13
  const { resolveSafeOutputDir } = require('../lib/workspace-safety');
package/commands/push.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const crypto = require('crypto');
4
3
  const readline = require('readline');
5
4
  const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
6
5
  const { apiRequestJson } = require('../utils/api');
package/commands/radar.js CHANGED
@@ -685,10 +685,6 @@ function taskBinding(source, scope = null) {
685
685
  return { task_source: source, task_scope: scope };
686
686
  }
687
687
 
688
- function taskBindingForProjection(task) {
689
- return task ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null);
690
- }
691
-
692
688
  function taskSourceLabel(sources = []) {
693
689
  if (sources.includes('member_loop_lock')) return 'member loop lock';
694
690
  if (sources.includes('mission_lock')) return 'mission lock';
package/commands/recap.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  taskCommand,
14
14
  } = require('../lib/first-minute');
15
15
  const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
16
+ const { buildAgentToday, renderAgentToday, hasActivity, ranAgent } = require('../lib/agent-today');
16
17
 
17
18
  const DAY_MS = 24 * 60 * 60 * 1000;
18
19
  const DEFAULT_DAYS = 7;
@@ -38,7 +39,8 @@ function readProjection(root) {
38
39
 
39
40
  function loadTasks(root) {
40
41
  const taskDb = loadTaskDb();
41
- if (taskDb) {
42
+ // Reading never creates the db: a first run before any setup writes nothing.
43
+ if (taskDb && fs.existsSync(taskDb.getDbPath())) {
42
44
  try {
43
45
  const db = taskDb.open();
44
46
  const ws = taskDb.workspaceRoot(root);
@@ -335,11 +337,14 @@ function printRecapHelp() {
335
337
  atris recap - what got done, in spoken lines
336
338
 
337
339
  atris recap a few spoken lines: waiting for your ok, still being checked
340
+ atris recap today what your ai coding agent did in this folder today
338
341
  atris recap --verbose the old report
339
342
  atris recap --days 30 widen the window
340
343
  atris recap --share paste-ready summary for Slack, email, or a customer
341
344
  atris recap --json structured output for agents and dashboards
342
345
 
346
+ in a folder with no atris setup, recap shows today by default.
347
+ today reads claude code and codex logs on this computer plus git. it sends nothing.
343
348
  certified work names atris task accept. uncertified stays still being checked.
344
349
  use --verbose or --share for the old report.
345
350
  `);
@@ -351,6 +356,11 @@ function recapAtris(args = []) {
351
356
  return;
352
357
  }
353
358
  const cwd = process.cwd();
359
+ if (args[0] === 'today') {
360
+ const today = buildAgentToday({ cwd });
361
+ console.log(args.includes('--json') ? JSON.stringify(today, null, 2) : renderAgentToday(today));
362
+ return;
363
+ }
354
364
  const taskDb = loadTaskDb();
355
365
  let root = cwd;
356
366
  try {
@@ -370,6 +380,20 @@ function recapAtris(args = []) {
370
380
  const daysIdx = args.indexOf('--days');
371
381
  const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
372
382
  const data = buildRecapData(root, { days });
383
+ // Before any setup, or when the board has nothing to say, the useful
384
+ // first minute is what your ai agent already did in this folder today.
385
+ const boardQuiet = data.empty
386
+ || ['shipped', 'waiting', 'checking', 'inProgress'].every((k) => !(data[k] || []).length);
387
+ if (boardQuiet && !args.includes('--verbose') && !args.includes('--full') && !args.includes('--share')) {
388
+ const today = buildAgentToday({ cwd });
389
+ // A brand new folder already names its last commit in the first
390
+ // minute, so commits alone keep that; an ai session here wins.
391
+ const fresh = data.empty && isFreshWorkspace(root) && !inProjectSubdir;
392
+ if (hasActivity(today) && (ranAgent(today) || !fresh)) {
393
+ console.log(args.includes('--json') ? JSON.stringify(today, null, 2) : renderAgentToday(today));
394
+ return;
395
+ }
396
+ }
373
397
  if (
374
398
  data.empty
375
399
  && isFreshWorkspace(root)
@@ -110,10 +110,6 @@ function buildDoneTasks(root, cutoff) {
110
110
  });
111
111
  }
112
112
 
113
- function buildLandings(root, cutoff) {
114
- return buildDoneTasks(root, cutoff).filter(task => task.landed);
115
- }
116
-
117
113
  function landingStatusText(task) {
118
114
  const status = String(task?.landing_status || 'unchecked').replace(/_/g, ' ');
119
115
  const reason = String(task?.landing_reason || '').replace(/_/g, ' ');
@@ -9,7 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const { execSync, spawnSync } = require('child_process');
12
+ const { spawnSync } = require('child_process');
13
13
 
14
14
  function findReviewEngine() {
15
15
  // Look for review_engine.py in common locations
package/commands/setup.js CHANGED
@@ -1,5 +1,3 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
1
  const { loadCredentials, promptUser } = require('../utils/auth');
4
2
  const { apiRequestJson } = require('../utils/api');
5
3
 
@@ -73,6 +71,12 @@ async function setupAtris() {
73
71
  token: creds.token,
74
72
  });
75
73
 
74
+ if (!result.ok && result.status === 401) {
75
+ console.error('\n Your saved login has expired, so Atris could not list your businesses.');
76
+ console.error(' Run "atris login", then "atris setup" again.');
77
+ process.exit(1);
78
+ }
79
+
76
80
  if (!result.ok) {
77
81
  console.error(`\n Could not fetch businesses: ${result.error || 'Unknown error'}`);
78
82
  console.error(' You can add one later with: atris business add <slug>');
package/commands/slop.js CHANGED
@@ -418,9 +418,15 @@ function findDeadCode(root = process.cwd(), opts = {}) {
418
418
  const candidates = scanDirs.flatMap((d) => listJsFiles(d));
419
419
  if (!candidates.length) return { root, candidates: 0, dead: [], testOnly: [] };
420
420
 
421
- // 1) reachability BFS from entrypoints over static edges
421
+ // 1) reachability BFS from entrypoints over static edges. A file that runs
422
+ // itself when started directly (`node lib/x.js`, guarded by
423
+ // `require.main === module`) is an entrypoint too, even if only a test
424
+ // imports it.
422
425
  const reached = new Set();
423
- const queue = [...deadEntrypoints(root)];
426
+ const runsItself = (f) => {
427
+ try { return /\brequire\.main\s*===\s*module\b/.test(fs.readFileSync(f, 'utf8')); } catch { return false; }
428
+ };
429
+ const queue = [...deadEntrypoints(root), ...candidates.filter(runsItself)];
424
430
  while (queue.length) {
425
431
  const f = queue.pop();
426
432
  if (reached.has(f)) continue;