atris 3.58.6 → 3.58.8

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 (90) hide show
  1. package/GETTING_STARTED.md +2 -2
  2. package/PERSONA.md +11 -11
  3. package/README.md +9 -1
  4. package/atris/GEMINI.md +7 -6
  5. package/atris/PERSONA.md +4 -4
  6. package/atris/features/README.md +77 -6
  7. package/atris/policies/engineering-principles.md +129 -0
  8. package/atris/policies/genesis.md +112 -0
  9. package/atris/policies/product-design-principles.md +100 -0
  10. package/atris/skills/README.md +44 -15
  11. package/atris/skills/aeo/SKILL.md +10 -10
  12. package/atris/skills/atris/SKILL.md +1 -1
  13. package/atris/skills/design/SKILL.md +3 -1
  14. package/atris/skills/engines/SKILL.md +42 -50
  15. package/atris/skills/meta/SKILL.md +4 -4
  16. package/atris/skills/youtube/SKILL.md +11 -11
  17. package/bin/atris.js +60 -8
  18. package/commands/aeo.js +9 -12
  19. package/commands/analytics.js +8 -11
  20. package/commands/auth.js +8 -2
  21. package/commands/autopilot.js +4 -3
  22. package/commands/avail.js +1 -1
  23. package/commands/brain.js +4 -1
  24. package/commands/business.js +13 -8
  25. package/commands/close.js +2 -1
  26. package/commands/computer.js +15 -10
  27. package/commands/context-sync.js +3 -3
  28. package/commands/design.js +362 -0
  29. package/commands/doc-health.js +377 -0
  30. package/commands/drive.js +32 -0
  31. package/commands/experiments.js +13 -4
  32. package/commands/improve.js +68 -2
  33. package/commands/integrations.js +2 -5
  34. package/commands/land.js +144 -4
  35. package/commands/learn.js +7 -2
  36. package/commands/member.js +239 -58
  37. package/commands/mission.js +94 -18
  38. package/commands/pack.js +2 -2
  39. package/commands/probe.js +11 -5
  40. package/commands/pull.js +1 -1
  41. package/commands/pulse.js +38 -0
  42. package/commands/push.js +1 -1
  43. package/commands/rainmaker.js +3 -16
  44. package/commands/rsi.js +156 -0
  45. package/commands/spaceship.js +1 -1
  46. package/commands/stream.js +27 -2
  47. package/commands/study.js +15 -9
  48. package/commands/sync.js +1 -1
  49. package/commands/task.js +54 -3
  50. package/commands/workflow.js +17 -7
  51. package/commands/youtube.js +76 -8
  52. package/lib/apply-gate.js +22 -4
  53. package/lib/codex-flight.js +28 -5
  54. package/lib/daily-log.js +88 -0
  55. package/lib/design-api.js +130 -0
  56. package/lib/engine-ask.js +1 -1
  57. package/lib/first-minute.js +1 -6
  58. package/lib/fleet.js +4 -1
  59. package/lib/improve-vitals-html.js +3 -2
  60. package/lib/known-commands.js +3 -3
  61. package/lib/lesson-contradiction.js +2 -2
  62. package/lib/map-refs.js +183 -0
  63. package/lib/member-context.js +42 -0
  64. package/lib/mission-room.js +4 -4
  65. package/lib/noninteractive.js +15 -4
  66. package/lib/policy-lessons.js +1 -1
  67. package/lib/receipt-block.js +137 -28
  68. package/lib/rsi-record.js +335 -0
  69. package/lib/runner-command.js +1 -1
  70. package/lib/scorecard.js +4 -4
  71. package/lib/sleep-sync.js +11 -0
  72. package/lib/state-detection.js +36 -17
  73. package/lib/task-db.js +74 -53
  74. package/lib/task-list-keeper.js +192 -0
  75. package/lib/todo-fallback.js +10 -4
  76. package/lib/todo.js +22 -10
  77. package/lib/wish-delegate.js +19 -5
  78. package/mcp/atris-mcp/index.mjs +227 -0
  79. package/package.json +4 -2
  80. package/scripts/det/det.js +2 -2
  81. package/scripts/det/pr-description.js +2 -2
  82. package/scripts/det/test.js +2 -2
  83. package/scripts/det/ytnotes +43 -8
  84. package/utils/api.js +1 -1
  85. package/utils/auth.js +79 -14
  86. package/utils/backend-root.js +31 -0
  87. package/utils/config.js +5 -4
  88. package/utils/owner-identity.js +61 -0
  89. package/atris/policies/LESSONS.md +0 -13
  90. package/atris/skills/autopilot/atris-autopilot.md +0 -71
@@ -293,6 +293,31 @@ function pushEvent(events, input) {
293
293
  if (event) events.push({ ...event, key: eventKey(event) });
294
294
  }
295
295
 
296
+ // When timestamps match, state snapshots precede receipts, then landing summaries.
297
+ const STREAM_SOURCE_RANK = {
298
+ task: 20,
299
+ task_episode: 30,
300
+ scorecard: 40,
301
+ mission_event: 50,
302
+ mission_state: 60,
303
+ mission_receipt: 70,
304
+ mission_landing: 80,
305
+ worktree: 85,
306
+ landing: 90,
307
+ state: 10,
308
+ };
309
+
310
+ function compareStreamEvents(a, b) {
311
+ const byMs = a.ms - b.ms;
312
+ if (byMs) return byMs;
313
+ const rankA = STREAM_SOURCE_RANK[a.source] ?? 55;
314
+ const rankB = STREAM_SOURCE_RANK[b.source] ?? 55;
315
+ if (rankA !== rankB) return rankA - rankB;
316
+ const byKey = String(a.key || '').localeCompare(String(b.key || ''));
317
+ if (byKey) return byKey;
318
+ return String(a.event || '').localeCompare(String(b.event || ''));
319
+ }
320
+
296
321
  function statePaths(root) {
297
322
  const stateDir = path.join(root, '.atris', 'state');
298
323
  return {
@@ -455,7 +480,6 @@ function collectRunReceiptEvents(root, deps, events) {
455
480
 
456
481
  function candidateWorktreeDirs(root, deps) {
457
482
  const dirs = new Set([
458
- path.resolve(root, '..', '.agent-worktrees'),
459
483
  path.join(path.dirname(root), '.agent-worktrees', path.basename(root)),
460
484
  ]);
461
485
  const sidecar = readJson(statePaths(root).sidecar, deps, null);
@@ -606,7 +630,7 @@ function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', no
606
630
  if (wantedAgent && event.agent.toLowerCase() !== wantedAgent) continue;
607
631
  byKey.set(event.key, event);
608
632
  }
609
- return [...byKey.values()].sort((a, b) => (a.ms - b.ms) || a.event.localeCompare(b.event));
633
+ return [...byKey.values()].sort(compareStreamEvents);
610
634
  }
611
635
 
612
636
  function waitingOnOperator(tasks) {
@@ -841,6 +865,7 @@ function streamCommand(args = [], deps = defaultDeps()) {
841
865
  module.exports = {
842
866
  collectSnapshot,
843
867
  collectStreamEvents,
868
+ compareStreamEvents,
844
869
  createStreamState,
845
870
  parseArgs,
846
871
  parseSince,
package/commands/study.js CHANGED
@@ -15,6 +15,7 @@ const fs = require('fs');
15
15
  const path = require('path');
16
16
  const http = require('http');
17
17
  const { spawn, spawnSync } = require('child_process');
18
+ const { resolveBackendRoot } = require('../utils/backend-root');
18
19
 
19
20
  const FEED_URL = 'http://localhost:8777';
20
21
  const FEED_STATS_URL = `${FEED_URL}/api/stats`;
@@ -91,15 +92,19 @@ function parseArgs(argv = []) {
91
92
  }
92
93
 
93
94
  function resolveStudyRoot(cwd = process.cwd()) {
94
- const candidates = [
95
- process.env.ATRIS_STUDY_ROOT,
96
- process.env.ATRIS_BACKEND_ROOT,
97
- path.join(path.resolve(cwd), 'atris', 'study'),
98
- ].filter(Boolean);
99
- for (const root of candidates) {
100
- const resolved = path.resolve(String(root));
101
- if (fs.existsSync(resolved)) return resolved;
102
- }
95
+ if (process.env.ATRIS_STUDY_ROOT) {
96
+ const explicit = path.resolve(process.env.ATRIS_STUDY_ROOT);
97
+ if (fs.existsSync(explicit) && fs.statSync(explicit).isDirectory()) return explicit;
98
+ }
99
+ // Same order as before: an explicitly set backend root wins over the local folder.
100
+ if (process.env.ATRIS_BACKEND_ROOT) {
101
+ const envBackend = path.resolve(process.env.ATRIS_BACKEND_ROOT);
102
+ if (fs.existsSync(envBackend)) return envBackend;
103
+ }
104
+ const local = path.join(path.resolve(cwd), 'atris', 'study');
105
+ if (fs.existsSync(local) && fs.statSync(local).isDirectory()) return local;
106
+ const backend = resolveBackendRoot(cwd);
107
+ if (backend) return backend;
103
108
  return null;
104
109
  }
105
110
 
@@ -697,6 +702,7 @@ async function run(argv = []) {
697
702
 
698
703
  module.exports = {
699
704
  run,
705
+ resolveStudyRoot,
700
706
  parseArgs,
701
707
  showHelp,
702
708
  wrapText,
package/commands/sync.js CHANGED
@@ -615,7 +615,7 @@ Detect: Project has \`atris/\` folder with MAP.md, TODO.md, PERSONA.md
615
615
  Commands: plan → do → review
616
616
 
617
617
  Key behaviors:
618
- - Read PERSONA.md (3-4 sentences, ASCII visuals)
618
+ - Read PERSONA.md (MAP.md first, plain words, lead with the outcome)
619
619
  - Check MAP.md for file:line refs
620
620
  - Use \`atris task\` for claims, proof, ready, and accept
621
621
  - Give every task a plain first layer: what changes, why it matters, and what
package/commands/task.js CHANGED
@@ -253,6 +253,7 @@ atris task - durable local task state (SQLite, gitignored)
253
253
  Sweep off-roadmap/duplicate work as archived (not failed);
254
254
  --from-failed opts in to relabel a fail-closed row (never done)
255
255
  atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
256
+ atris task keep [--json] Put away finished and untouched work, then refresh the list
256
257
  atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
257
258
  atris task relabel-archived [--dry-run|--apply]
258
259
  One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
@@ -633,6 +634,12 @@ function goldenPathMissionXpTask(task) {
633
634
  && /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
634
635
  }
635
636
 
637
+ // A receipt that cannot be read drops out of review evidence; name it under
638
+ // ATRIS_DEBUG so the gap is findable, matching lib/todo.js.
639
+ function debugUnreadableReceipt(rel, err) {
640
+ if (process.env.ATRIS_DEBUG) console.error(`[task] receipt unreadable, skipped: ${rel}: ${err && err.message}`);
641
+ }
642
+
636
643
  function receiptTextForProof(proof, root = process.cwd()) {
637
644
  const chunks = [];
638
645
  const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
@@ -651,7 +658,9 @@ function receiptTextForProof(proof, root = process.cwd()) {
651
658
  last_landing: parsed.last_landing || null,
652
659
  summary: parsed.summary || null,
653
660
  }).slice(0, 12000));
654
- } catch {}
661
+ } catch (err) {
662
+ debugUnreadableReceipt(rel, err);
663
+ }
655
664
  }
656
665
  return chunks.join(' ');
657
666
  }
@@ -671,7 +680,10 @@ function missionReceiptResultForProof(task, proof, root = process.cwd()) {
671
680
  try {
672
681
  const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
673
682
  landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
674
- } catch { continue; }
683
+ } catch (err) {
684
+ debugUnreadableReceipt(rel, err);
685
+ continue;
686
+ }
675
687
  const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
676
688
  if (!changed) continue;
677
689
  if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
@@ -6416,7 +6428,7 @@ function compactTechnicalDetails(task, formatTitle = value => value) {
6416
6428
  }
6417
6429
 
6418
6430
  function taskDayGroups(tasks, { now = Date.now() } = {}) {
6419
- const active = tasks.filter(task => task.status !== 'done');
6431
+ const active = tasks.filter(task => task.status !== 'done' && task.status !== 'archived');
6420
6432
  const staleFailed = [];
6421
6433
  const visible = [];
6422
6434
  for (const task of active) {
@@ -6446,7 +6458,43 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
6446
6458
  return { groups: grouped, staleFailed };
6447
6459
  }
6448
6460
 
6461
+ function refreshKeptTaskList(cwd = process.cwd()) {
6462
+ const taskDb = getTaskDb();
6463
+ const db = taskDb.open();
6464
+ writeDefaultProjection(taskDb, db);
6465
+ autoRenderTodoFromDb(cwd);
6466
+ }
6467
+
6468
+ function keptCount(result) {
6469
+ return (result && result.put_away ? result.put_away.length : 0)
6470
+ + (result && result.reaped ? result.reaped.length : 0);
6471
+ }
6472
+
6473
+ function cmdKeep(args) {
6474
+ const result = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
6475
+ if (keptCount(result)) refreshKeptTaskList(process.cwd());
6476
+ const count = keptCount(result);
6477
+ if (wantsJson(args)) {
6478
+ printJson({
6479
+ ok: true,
6480
+ action: 'keep',
6481
+ count,
6482
+ put_away: result.put_away,
6483
+ reaped: result.reaped,
6484
+ });
6485
+ return;
6486
+ }
6487
+ if (!count) {
6488
+ console.log('task list is current. nothing to put away.');
6489
+ return;
6490
+ }
6491
+ const noun = count === 1 ? 'item' : 'items';
6492
+ console.log(`put away ${count} ${noun} that were finished or sitting still.`);
6493
+ }
6494
+
6449
6495
  function cmdDay(args) {
6496
+ const kept = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
6497
+ if (keptCount(kept)) autoRenderTodoFromDb(process.cwd());
6450
6498
  const all = hasFlag(args, '--all');
6451
6499
  const full = hasFlag(args, '--full');
6452
6500
  const everywhere = taskScopeEverywhere(args);
@@ -13321,6 +13369,7 @@ async function runTaskCommand(args) {
13321
13369
  case 'fail': return cmdDone([...rest, '--failed']);
13322
13370
  case 'archive': return cmdArchive(rest);
13323
13371
  case 'clear-done': return cmdClearDone(rest);
13372
+ case 'keep': return cmdKeep(rest);
13324
13373
  case 'reap-mission-blockers':
13325
13374
  case 'reap-blockers':
13326
13375
  return cmdReapMissionBlockers(rest);
@@ -13425,6 +13474,8 @@ module.exports = {
13425
13474
  delegateTask,
13426
13475
  AGENT_ENV_MARKERS,
13427
13476
  autoRenderTodoFromDb,
13477
+ refreshKeptTaskList,
13478
+ keptCount,
13428
13479
  projectionMissions,
13429
13480
  projectionWishes,
13430
13481
  taskBoardViewModel,
@@ -21,6 +21,19 @@ const { loadContext } = require('../lib/state-detection');
21
21
  const { buildToolResultBody } = require('../lib/tool-result-encode');
22
22
  const { commitReviewLearning } = require('./learn');
23
23
 
24
+ function reviewLearningLine(timestamp, answer) {
25
+ return `- ${timestamp} - ${String(answer).trim().replace(/\s*\u2014\s*/g, ', ')}`;
26
+ }
27
+
28
+ function appendReviewLearningToJournal(journalContent, learning) {
29
+ const text = String(journalContent || '').replace(/\r\n/g, '\n');
30
+ const note = String(learning || '');
31
+ if (text.includes('## Notes')) {
32
+ return text.replace(/## Notes\n/, `## Notes\n${note}\n`);
33
+ }
34
+ return `${text}\n## Notes\n${note}\n`;
35
+ }
36
+
24
37
  function wrapWorkflowText(text, width = 76) {
25
38
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
26
39
  if (!normalized) return [''];
@@ -1773,14 +1786,9 @@ async function reviewAtris() {
1773
1786
  if (fs.existsSync(logFile)) {
1774
1787
  let journalContent = fs.readFileSync(logFile, 'utf8');
1775
1788
  const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
1776
- const learning = `- ${timestamp} \u2014 ${answer.trim()}`;
1789
+ const learning = reviewLearningLine(timestamp, answer);
1777
1790
 
1778
- // Find or create ## Notes section
1779
- if (journalContent.includes('## Notes')) {
1780
- journalContent = journalContent.replace(/## Notes\n/, `## Notes\n${learning}\n`);
1781
- } else {
1782
- journalContent += `\n## Notes\n${learning}\n`;
1783
- }
1791
+ journalContent = appendReviewLearningToJournal(journalContent, learning);
1784
1792
 
1785
1793
  fs.writeFileSync(logFile, journalContent);
1786
1794
  console.log('');
@@ -1893,6 +1901,8 @@ module.exports = {
1893
1901
  planAtris,
1894
1902
  doAtris,
1895
1903
  reviewAtris,
1904
+ appendReviewLearningToJournal,
1905
+ reviewLearningLine,
1896
1906
  renderReviewMinute,
1897
1907
  executorAgentPrompt,
1898
1908
  executorDispatchForTask,
@@ -283,6 +283,51 @@ function parseVttTimestampMs(value) {
283
283
  return ((hours * 3600) + (minutes * 60) + seconds) * 1000 + millis;
284
284
  }
285
285
 
286
+ function parseCleanTimestampMs(value) {
287
+ const match = String(value || '').trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
288
+ if (!match) return null;
289
+ const hours = Number(match[1] || 0);
290
+ const minutes = Number(match[2] || 0);
291
+ const seconds = Number(match[3] || 0);
292
+ if (![hours, minutes, seconds].every(Number.isFinite)) return null;
293
+ if (minutes > 59 || seconds > 59) return null;
294
+ return ((hours * 3600) + (minutes * 60) + seconds) * 1000;
295
+ }
296
+
297
+ function parseCleanTranscriptCues(raw) {
298
+ const cues = [];
299
+ let startMs = 0;
300
+ let sawStamp = false;
301
+ const pending = [];
302
+ const flush = () => {
303
+ const text = pending.join(' ').replace(/\s+/g, ' ').trim();
304
+ pending.length = 0;
305
+ if (!text) return;
306
+ const cue = { startMs, text };
307
+ if (cues.length && cues[cues.length - 1].text === cue.text && cues[cues.length - 1].startMs === cue.startMs) {
308
+ return;
309
+ }
310
+ cues.push(cue);
311
+ };
312
+ for (const line of String(raw).split(/\r?\n/)) {
313
+ const stripped = line.trim();
314
+ if (!stripped) continue;
315
+ const stamp = stripped.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]$/);
316
+ if (stamp) {
317
+ flush();
318
+ const parsed = parseCleanTimestampMs(stamp[1]);
319
+ if (parsed != null) {
320
+ startMs = parsed;
321
+ sawStamp = true;
322
+ }
323
+ continue;
324
+ }
325
+ pending.push(stripped.replace(/<[^>]+>/g, ''));
326
+ }
327
+ flush();
328
+ return sawStamp && cues.length ? cues : [];
329
+ }
330
+
286
331
  function fetchCaptionText(urlString, redirects = 0) {
287
332
  if (!captionHostAllowed(urlString)) {
288
333
  return Promise.resolve(null);
@@ -380,7 +425,7 @@ function parseCaptionCues(raw) {
380
425
  return cues;
381
426
  }
382
427
 
383
- return [];
428
+ return parseCleanTranscriptCues(raw);
384
429
  }
385
430
 
386
431
  function parseCaptionText(raw) {
@@ -415,7 +460,14 @@ function parseCaptionText(raw) {
415
460
  }
416
461
 
417
462
  function parseYtDlpInfoJson(result) {
418
- const raw = String((result && result.stdout) || '').trim();
463
+ const kept = [];
464
+ for (const line of String((result && result.stdout) || '').split(/\r?\n/)) {
465
+ const trimmed = line.trim();
466
+ if (!trimmed) continue;
467
+ if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
468
+ kept.push(trimmed);
469
+ }
470
+ const raw = kept.join('\n').trim();
419
471
  if (!raw) return null;
420
472
  try {
421
473
  const info = JSON.parse(raw);
@@ -426,7 +478,7 @@ function parseYtDlpInfoJson(result) {
426
478
  }
427
479
 
428
480
  function localCaptionNames(id) {
429
- // scripts/det/ytnotes keeps the same VTT names (not clean.txt).
481
+ // scripts/det/ytnotes keeps these VTT names plus leftover yt_<id>.clean.txt.
430
482
  return [
431
483
  `yt_${id}.en.vtt`,
432
484
  `yt_${id}.en-orig.vtt`,
@@ -765,6 +817,10 @@ function applySidecarRel(id) {
765
817
  return applyGate.applySidecarRel('youtube', id);
766
818
  }
767
819
 
820
+ function notesApplyRel(id) {
821
+ return applyGate.applySidecarRel('notes', experimentIdToken(id));
822
+ }
823
+
768
824
  function notesExperimentSlug(id) {
769
825
  return `notes-${experimentIdToken(id)}`;
770
826
  }
@@ -814,7 +870,7 @@ function saveRichNotes(url, deps = {}) {
814
870
  url,
815
871
  lesson,
816
872
  slug: id ? notesExperimentSlug(id) : null,
817
- applyRel: id ? applySidecarRel(id) : null,
873
+ applyRel: id ? notesApplyRel(id) : null,
818
874
  });
819
875
  return { thin: false, brief, packRel, lesson };
820
876
  }
@@ -826,7 +882,7 @@ function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
826
882
  return applyGate.ensureApply({
827
883
  cwd,
828
884
  source: url,
829
- rel: id ? applySidecarRel(id) : null,
885
+ rel: id ? notesApplyRel(id) : null,
830
886
  now,
831
887
  output,
832
888
  incompleteMessage: slug
@@ -907,6 +963,7 @@ function unsaveYoutubeNotes(target, deps = {}) {
907
963
  };
908
964
  add(briefRel);
909
965
  add(applyRel);
966
+ add(notesApplyRel(id));
910
967
  for (const section of sections) {
911
968
  add(teachBriefRel(id, section));
912
969
  add(applySidecarRel(`${id}-s${section}`));
@@ -959,6 +1016,7 @@ function ensureProcessApply({ cwd, url, now, output } = {}) {
959
1016
  output,
960
1017
  incompleteMessage: PROCESS_APPLY_MESSAGE,
961
1018
  required: true,
1019
+ human: true,
962
1020
  });
963
1021
  }
964
1022
 
@@ -1455,15 +1513,22 @@ function channelVideosUrl(channel) {
1455
1513
  return `${base}/videos`;
1456
1514
  }
1457
1515
 
1516
+ function looksLikeFlatVideoId(id) {
1517
+ const text = String(id || '');
1518
+ if (/^(NA|None)$/i.test(text)) return false;
1519
+ return /^[A-Za-z0-9_-]+$/.test(text);
1520
+ }
1521
+
1458
1522
  function parseFlatPlaylist(stdout) {
1459
1523
  const videos = [];
1460
1524
  for (const line of String(stdout || '').split(/\r?\n/)) {
1461
1525
  const trimmed = line.trim();
1462
1526
  if (!trimmed || !trimmed.includes('|')) continue;
1527
+ if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
1463
1528
  const idx = trimmed.indexOf('|');
1464
1529
  const id = trimmed.slice(0, idx).trim();
1465
1530
  const title = trimmed.slice(idx + 1).trim();
1466
- if (id && id !== 'NA') videos.push({ id, title });
1531
+ if (looksLikeFlatVideoId(id)) videos.push({ id, title });
1467
1532
  }
1468
1533
  return videos;
1469
1534
  }
@@ -1744,6 +1809,7 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
1744
1809
  const result = spawn('yt-dlp', [
1745
1810
  '--no-update',
1746
1811
  '--flat-playlist',
1812
+ '--no-warnings',
1747
1813
  '--print',
1748
1814
  '%(id)s|%(title)s',
1749
1815
  playlistUrl,
@@ -1938,7 +2004,7 @@ function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
1938
2004
  }));
1939
2005
  const baseline = proveSavedLearnerBaseline({
1940
2006
  cwd: deps.cwd || process.cwd(),
1941
- applyRel: id ? applySidecarRel(id) : null,
2007
+ applyRel: id ? notesApplyRel(id) : null,
1942
2008
  lesson,
1943
2009
  output,
1944
2010
  json: asJson,
@@ -1989,7 +2055,7 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
1989
2055
  const id = videoIdFromUrl(url);
1990
2056
  const baseline = proveSavedLearnerBaseline({
1991
2057
  cwd,
1992
- applyRel: id ? applySidecarRel(id) : null,
2058
+ applyRel: id ? notesApplyRel(id) : null,
1993
2059
  lesson: saved.lesson,
1994
2060
  output,
1995
2061
  json: deps.json === true,
@@ -2129,10 +2195,12 @@ function parseSearchStdout(stdout = '') {
2129
2195
  for (const line of String(stdout || '').split(/\r?\n/)) {
2130
2196
  const trimmed = line.trim();
2131
2197
  if (!trimmed || !trimmed.includes('|')) continue;
2198
+ if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
2132
2199
  const parts = trimmed.split(/\s*\|\s*/).map((part) => part.trim());
2133
2200
  if (parts.length < 5) continue;
2134
2201
  const url = parts[parts.length - 1];
2135
2202
  if (!/^https?:\/\/(?:www\.)?(?:youtube\.com\/|youtu\.be\/)/i.test(url)) continue;
2203
+ if (!looksLikeFlatVideoId(videoIdFromUrl(url))) continue;
2136
2204
  const row = {
2137
2205
  title: parts[0] || '',
2138
2206
  channel: parts[1] || '',
package/lib/apply-gate.js CHANGED
@@ -26,6 +26,17 @@ function isFilledApply(fields) {
26
26
  return !empty(fields.change) && !empty(fields.receipt);
27
27
  }
28
28
 
29
+ function isLearnerKeepApply(fields) {
30
+ const change = String((fields && fields.change) || '');
31
+ const receipt = String((fields && fields.receipt) || '');
32
+ return /^apply\s+atris\/experiments\//i.test(change)
33
+ || /keep only if measure\.py moves 0/.test(receipt);
34
+ }
35
+
36
+ function isFilledHumanApply(fields) {
37
+ return isFilledApply(fields) && !isLearnerKeepApply(fields);
38
+ }
39
+
29
40
  function applySlug(text) {
30
41
  const slug = String(text || '')
31
42
  .toLowerCase()
@@ -46,7 +57,7 @@ function readApplyReceipt({ cwd, rel } = {}) {
46
57
  return { rel, text: fs.readFileSync(abs, 'utf8') };
47
58
  }
48
59
 
49
- function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine } = {}) {
60
+ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine, force } = {}) {
50
61
  try {
51
62
  if (!rel || !cwd) return null;
52
63
  const wikiDir = path.join(cwd, 'atris', 'wiki');
@@ -56,7 +67,7 @@ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine }
56
67
  fs.mkdirSync(path.dirname(abs), { recursive: true });
57
68
  const changeText = change || 'fill this';
58
69
  const receiptText = receipt || 'fill this';
59
- let shouldWrite = !fs.existsSync(abs);
70
+ let shouldWrite = force === true || !fs.existsSync(abs);
60
71
  if (!shouldWrite && change && receipt) {
61
72
  shouldWrite = !isFilledApply(parseApplyFields(fs.readFileSync(abs, 'utf8')));
62
73
  }
@@ -86,11 +97,16 @@ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine }
86
97
 
87
98
  function ensureApply({
88
99
  cwd, source, rel, now, output, incompleteMessage, required = true, change, receipt, journalLine,
100
+ human = false,
89
101
  } = {}) {
90
102
  const print = typeof output === 'function' ? output : (line = '') => console.error(line);
91
103
  const existing = readApplyReceipt({ cwd, rel });
92
- if (existing && isFilledApply(parseApplyFields(existing.text))) return 0;
93
- if (!(required && existing)) writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine });
104
+ const fields = existing ? parseApplyFields(existing.text) : null;
105
+ const keepSidecar = Boolean(human && fields && isLearnerKeepApply(fields));
106
+ if (existing && isFilledApply(fields) && !keepSidecar) return 0;
107
+ if (!(required && existing) || keepSidecar) {
108
+ writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine, force: keepSidecar });
109
+ }
94
110
  print(incompleteMessage);
95
111
  return required ? 2 : 0;
96
112
  }
@@ -112,4 +128,6 @@ module.exports = {
112
128
  ensureApply,
113
129
  ephemeralApplyMessage,
114
130
  hintEphemeralApply,
131
+ isFilledHumanApply,
132
+ isLearnerKeepApply,
115
133
  };
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { spawn, spawnSync } = require('child_process');
6
7
 
7
- const CODEX_COMPANION = '/Users/keshavrao/.claude/plugins/cache/openai-codex/codex/1.0.4/scripts/codex-companion.mjs';
8
+ // Worktree flights need full file access to edit and ship the requested repo.
9
+ const FLIGHT_SANDBOX = 'danger-full-access';
8
10
  const CONTRACT = "CRITICAL: complete the entire job in this single run; never end your reply on a plan. Do NOT run any atris boot/brain/state commands; the only atris commands allowed are 'atris worktree guard' and 'atris worktree ship'.";
9
11
  const COAUTHOR = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
10
12
 
@@ -96,12 +98,30 @@ function parseTaskId(stdout) {
96
98
  return asMatch ? asMatch[1] : '';
97
99
  }
98
100
 
99
- function spawnCodex(worktreePath, prompt) {
101
+ function resolveCodexCompanion() {
102
+ if (process.env.ATRIS_CODEX_COMPANION) {
103
+ const override = path.resolve(process.env.ATRIS_CODEX_COMPANION);
104
+ return fs.existsSync(override) && fs.statSync(override).isFile() ? override : null;
105
+ }
106
+ const cache = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'openai-codex', 'codex');
107
+ if (!fs.existsSync(cache)) return null;
108
+ const versions = fs.readdirSync(cache, { withFileTypes: true })
109
+ .filter((entry) => entry.isDirectory())
110
+ .map((entry) => entry.name)
111
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
112
+ for (const version of versions) {
113
+ const script = path.join(cache, version, 'scripts', 'codex-companion.mjs');
114
+ if (fs.existsSync(script) && fs.statSync(script).isFile()) return script;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ function spawnCodex(worktreePath, prompt, companion) {
100
120
  return new Promise((resolve, reject) => {
101
- const argv = [CODEX_COMPANION, 'task', '--background', '--write', prompt];
121
+ const argv = [companion, 'task', '--background', '--write', prompt];
102
122
  const child = spawn('node', argv, {
103
123
  cwd: worktreePath,
104
- env: { ...process.env, CODEX_COMPANION_SANDBOX: 'danger-full-access' },
124
+ env: { ...process.env, CODEX_COMPANION_SANDBOX: FLIGHT_SANDBOX },
105
125
  stdio: ['ignore', 'pipe', 'pipe'],
106
126
  });
107
127
  let stdout = '';
@@ -143,12 +163,15 @@ async function dispatchCodexFlight({ repoPath, slug, brief, verifyCmd, dryRun =
143
163
  return { dryRun: true, prompt };
144
164
  }
145
165
 
166
+ const companion = resolveCodexCompanion();
167
+ if (!companion) throw new Error('codex companion not found. set ATRIS_CODEX_COMPANION to its script path.');
146
168
  runWorktreeAdd(repo.repoPath, branch, worktreePath);
147
- const task = await spawnCodex(worktreePath, prompt);
169
+ const task = await spawnCodex(worktreePath, prompt, companion);
148
170
  return { taskId: task.taskId, worktreePath, branch };
149
171
  }
150
172
 
151
173
  module.exports = {
152
174
  dispatchCodexFlight,
153
175
  buildPrompt,
176
+ resolveCodexCompanion,
154
177
  };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // Two-level daily logging. Member logs (atris/team/<member>/logs/<date>.md)
4
+ // keep the detailed workstream: claims, notes, result receipts, completions.
5
+ // The master journal (atris/logs/<YYYY>/<date>.md) stays curated and receives
6
+ // only consequential entries: terminal task events and explicitly major
7
+ // updates such as decisions, handoffs, and shipped work.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function todayLogName(now = new Date()) {
13
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
14
+ }
15
+
16
+ function logStamp(now = new Date()) {
17
+ return now.toTimeString().slice(0, 5);
18
+ }
19
+
20
+ function compactLogText(value, max = 240) {
21
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
22
+ if (!text) return '';
23
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
24
+ }
25
+
26
+ function logFieldRows(fields) {
27
+ return Object.entries(fields)
28
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
29
+ .map(([key, value]) => `- ${key}: ${compactLogText(value, 500)}`);
30
+ }
31
+
32
+ function appendDailyEntry(logPath, title, fields) {
33
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
34
+ fs.appendFileSync(logPath, [
35
+ `## ${logStamp()} · ${title}`,
36
+ ...logFieldRows(fields),
37
+ '',
38
+ ].join('\n'), 'utf8');
39
+ return logPath;
40
+ }
41
+
42
+ function memberSlug(root, member) {
43
+ const slug = String(member || '').trim();
44
+ if (!/^[A-Za-z0-9._-]+$/.test(slug)) return null;
45
+ if (!fs.existsSync(path.join(root, 'atris', 'team', slug, 'MEMBER.md'))) return null;
46
+ return slug;
47
+ }
48
+
49
+ function appendMemberDailyEntry(root, member, title, fields) {
50
+ const slug = memberSlug(root, member);
51
+ if (!slug || !fs.existsSync(path.join(root, 'atris'))) return null;
52
+ const logPath = path.join(root, 'atris', 'team', slug, 'logs', todayLogName());
53
+ appendDailyEntry(logPath, title, { ...fields, member: slug });
54
+ return logPath;
55
+ }
56
+
57
+ function appendMasterDailyEntry(root, title, fields) {
58
+ if (!fs.existsSync(path.join(root, 'atris'))) return null;
59
+ const logName = todayLogName();
60
+ const logPath = path.join(root, 'atris', 'logs', logName.slice(0, 4), logName);
61
+ appendDailyEntry(logPath, title, fields);
62
+ return logPath;
63
+ }
64
+
65
+ // Notes a person deliberately escalates to the day record. Anything else stays
66
+ // in the member's own log so the master journal does not become a transcript.
67
+ const MASTER_NOTE = /^(decision|decided|milestone|handoff|shipped|launched|landed|blocked)\b\s*[:\-]/i;
68
+
69
+ function noteTitle(content) {
70
+ const key = String(content || '').trim().match(/^([a-z]+)\s*[:\-]/i);
71
+ const word = key ? key[1].toLowerCase() : '';
72
+ if (word === 'decision' || word === 'decided') return 'Task decision';
73
+ if (word === 'handoff') return 'Task handoff';
74
+ if (word === 'shipped' || word === 'launched' || word === 'landed') return 'Task shipped';
75
+ if (word === 'blocked') return 'Task blocked';
76
+ if (word === 'milestone') return 'Task milestone';
77
+ return 'Task update';
78
+ }
79
+
80
+ module.exports = {
81
+ todayLogName,
82
+ compactLogText,
83
+ memberSlug,
84
+ appendMemberDailyEntry,
85
+ appendMasterDailyEntry,
86
+ MASTER_NOTE,
87
+ noteTitle,
88
+ };