atris 3.57.4 → 3.58.5

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 (174) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +55 -12
  6. package/atris/skills/x-search/SKILL.md +8 -6
  7. package/atris/skills/youtube/SKILL.md +37 -8
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +153 -23
  16. package/bin/atris.js +78 -30
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +7 -7
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +3 -3
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +12 -12
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +90 -82
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +97 -40
  98. package/commands/worktree.js +1 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +89 -17
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +601 -48
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/auto-accept-certified.js +28 -12
  106. package/lib/autoland.js +6 -6
  107. package/lib/bench/engines.js +59 -8
  108. package/lib/bench/report.js +1 -1
  109. package/lib/bench/runner.js +204 -5
  110. package/lib/bench/tree-render.js +76 -0
  111. package/lib/card.js +1 -1
  112. package/lib/chat-log-scan.js +1 -1
  113. package/lib/claude-boot-block.js +2 -0
  114. package/lib/deck-compose.js +1 -1
  115. package/lib/deck-history.js +2 -2
  116. package/lib/deck-layout.js +1 -1
  117. package/lib/deck-review.js +5 -5
  118. package/lib/deck-schema.js +1 -1
  119. package/lib/default-verifier.js +1 -1
  120. package/lib/developer-api.js +3 -0
  121. package/lib/engine-ask.js +45 -7
  122. package/lib/engine-registry.js +15 -2
  123. package/lib/file-ops.js +1 -1
  124. package/lib/fleet.js +71 -35
  125. package/lib/html-render.js +1 -1
  126. package/lib/intents.js +172 -0
  127. package/lib/journal.js +1 -1
  128. package/lib/known-commands.js +2 -2
  129. package/lib/land-green.js +364 -0
  130. package/lib/lesson-contradiction.js +2 -2
  131. package/lib/lesson-preflight.js +1 -1
  132. package/lib/manifest.js +5 -5
  133. package/lib/member-alive.js +9 -5
  134. package/lib/memory-view.js +3 -3
  135. package/lib/mission-protected-lane.js +1 -1
  136. package/lib/mission-root.js +2 -2
  137. package/lib/orb-context.js +10 -10
  138. package/lib/pack-capabilities.js +861 -3
  139. package/lib/permission-grants.js +2 -2
  140. package/lib/policy-lessons.js +6 -6
  141. package/lib/pulse.js +8 -8
  142. package/lib/receipt-evidence.js +1 -1
  143. package/lib/reel.js +1 -1
  144. package/lib/review-integrity.js +1 -1
  145. package/lib/runner-command.js +35 -5
  146. package/lib/scorecard.js +4 -4
  147. package/lib/security-scan.js +5 -5
  148. package/lib/self-drive.js +1 -1
  149. package/lib/site-publish.js +372 -0
  150. package/lib/site.js +1 -1
  151. package/lib/slides-deck.js +2 -2
  152. package/lib/state-detection.js +2 -2
  153. package/lib/sync-telemetry.js +2 -2
  154. package/lib/task-db.js +17 -10
  155. package/lib/task-explanation.js +1 -1
  156. package/lib/task-proof.js +2 -2
  157. package/lib/task-receipt.js +3 -3
  158. package/lib/theme.js +12 -12
  159. package/lib/todo-fallback.js +3 -3
  160. package/lib/todo-sections.js +1 -1
  161. package/lib/tree-hash.js +79 -0
  162. package/lib/usage.js +1 -1
  163. package/lib/wish-delegate.js +1 -1
  164. package/lib/workspace-safety.js +4 -4
  165. package/lib/workspace-scaffold.js +3 -3
  166. package/package.json +1 -1
  167. package/scripts/det/concierge-bakeoff.sh +26 -0
  168. package/scripts/det/ytquote-repair.js +0 -1
  169. package/scripts/det/ytrail-race.js +0 -5
  170. package/scripts/member-operate.mjs +52 -14
  171. package/scripts/outbound-artifact-gate.js +0 -3
  172. package/utils/auth.js +0 -3
  173. package/utils/config.js +0 -1
  174. package/scripts/det/checklist-score.js +0 -191
@@ -31,6 +31,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
31
31
  output(` ${commandName} search --paid "<query>" [--limit N] [--json]`);
32
32
  output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine] [--save]`);
33
33
  output(` ${commandName} teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]`);
34
+ output(` ${commandName} teach owed [--json]`);
35
+ output(` ${commandName} teach next [--json]`);
34
36
  output(` ${commandName} unsave <url-or-id>`);
35
37
  output(` ${commandName} process <youtube-url> [options]`);
36
38
  output(` ${commandName} digest [--days N]`);
@@ -40,14 +42,14 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
40
42
  output(` ${commandName} watch tick`);
41
43
  output(` ${commandName} <youtube-url> [options]`);
42
44
  output('');
43
- output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links');
44
- output('search --paid = 5 credits, watch permalinks + titles from Atris');
45
- output('notes = free local notes to stdout; ephemeral unless --save');
46
- output('teach = one chapter from local captions; ephemeral unless --save');
47
- output('rich ephemeral notes/teach print one apply next-step (no files)');
48
- output('process = 5 credits cloud knowledge (needs a filled Apply)');
49
- output('digest = one decision page from this week\'s video briefs');
50
- output('watch = subscribed channels turn into briefs without a human');
45
+ output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search prints one failing check; hands off to teach');
46
+ output('search --paid = 5 credits, watch permalinks + titles from Atris; rich paid search prints one failing check; hands off to teach');
47
+ output('notes = free local notes to stdout; ephemeral unless --save; hands off to teach');
48
+ output('teach = one chapter from local captions; bare teach resumes unpaid checks, then the next chapter after recap or skip');
49
+ output('rich ephemeral notes/teach print one apply next-step and one failing check (no files)');
50
+ output('process = 5 credits cloud knowledge (needs a filled Apply); rich process prints one failing check');
51
+ output('digest = one decision page from this week\'s video briefs; rich digest writes one apply and a failing keep/revert pack');
52
+ output('watch = subscribed channels turn into briefs without a human; add hands off to tick; tick hands off to teach when it briefed');
51
53
  output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
52
54
  output('Falls back to cloud video processing when local captions are unavailable.');
53
55
  output('');
@@ -58,6 +60,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
58
60
  output(' --section <n> Chapter to teach, 1-based (teach only, default: 1)');
59
61
  output(' --recap <text> Unlock the next teach section with the unpaid check');
60
62
  output(' --skip Unlock the next teach section without answering');
63
+ output(' owed Print the unpaid teach check (no network)');
64
+ output(' next Next unlocked chapter (no url, no billing)');
61
65
  output(' --unsave Delete filed brief, apply stub, and matching notes/teach experiment packs (no paid calls)');
62
66
  output(' --query, -q <text> Focus question for the analysis');
63
67
  output(' --agent <id> Agent id to store knowledge against');
@@ -77,6 +81,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
77
81
  output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID --save`);
78
82
  output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID"`);
79
83
  output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID" --section 2`);
84
+ output(` ${commandName} teach next`);
80
85
  output(` ${commandName} notes --unsave VIDEO_ID`);
81
86
  output(` ${commandName} unsave VIDEO_ID`);
82
87
  output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID https://youtu.be/OTHER_ID`);
@@ -507,6 +512,17 @@ async function processYoutube(options, deps = {}) {
507
512
  return result.data;
508
513
  }
509
514
 
515
+ function processAnalysisText(data) {
516
+ return data?.video_analysis || data?.analysis || data?.result || '';
517
+ }
518
+
519
+ function printProcessLearnerGate(data, { json = false } = {}, output) {
520
+ printLearnerCheckGate(output, notesLessonFromText(processAnalysisText(data)), {
521
+ includeCheck: true,
522
+ json,
523
+ });
524
+ }
525
+
510
526
  function formatYoutubeResult(data) {
511
527
  const lines = [];
512
528
  const metadata = data?.metadata || {};
@@ -526,7 +542,7 @@ function formatYoutubeResult(data) {
526
542
  const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
527
543
  lines.push(`Credits: ${used} used, ${remaining} remaining`);
528
544
  }
529
- const analysis = data?.video_analysis || data?.analysis || data?.result;
545
+ const analysis = processAnalysisText(data);
530
546
  if (analysis) {
531
547
  lines.push('');
532
548
  lines.push(String(analysis).trim());
@@ -567,22 +583,24 @@ function parseNotesArgs(argv = []) {
567
583
  let help = false;
568
584
  let save = false;
569
585
  let unsave = false;
586
+ let json = false;
570
587
  for (const raw of argv) {
571
588
  const arg = String(raw);
572
589
  if (arg === '--help' || arg === '-h' || arg === 'help') help = true;
573
590
  else if (arg === '--save') save = true;
574
591
  else if (arg === '--unsave') unsave = true;
592
+ else if (arg === '--json') json = true;
575
593
  }
576
594
  for (const raw of argv) {
577
595
  const arg = String(raw);
578
596
  if (arg === '--help' || arg === '-h' || arg === 'help') continue;
579
- if (arg === '--save' || arg === '--unsave') continue;
597
+ if (arg === '--save' || arg === '--unsave' || arg === '--json') continue;
580
598
  if (arg.startsWith('-')) continue;
581
599
  if (looksLikeYoutubeUrl(arg)) urls.push(arg);
582
600
  else if (unsave && videoIdFromArg(arg)) urls.push(arg);
583
601
  else engine = arg;
584
602
  }
585
- return { urls, engine, help, save, unsave };
603
+ return { urls, engine, help, save, unsave, json };
586
604
  }
587
605
 
588
606
  function dateStamp(now) {
@@ -680,7 +698,7 @@ function saveRichNotes(url, deps = {}) {
680
698
  const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
681
699
  const lesson = notesLessonFromText(readNotesText({ url, workDir }));
682
700
  if (isThinTeachLesson(lesson)) {
683
- return { thin: true, brief: null, packRel: null };
701
+ return { thin: true, brief: null, packRel: null, lesson };
684
702
  }
685
703
  const brief = fileNotesBrief(url, deps);
686
704
  const id = videoIdFromUrl(url);
@@ -691,20 +709,21 @@ function saveRichNotes(url, deps = {}) {
691
709
  slug: id ? notesExperimentSlug(id) : null,
692
710
  applyRel: id ? applySidecarRel(id) : null,
693
711
  });
694
- return { thin: false, brief, packRel };
712
+ return { thin: false, brief, packRel, lesson };
695
713
  }
696
714
 
697
715
  function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
698
716
  const id = videoIdFromUrl(url);
699
717
  const pack = packRel || (id ? notesExperimentRel(id) : null);
718
+ const slug = pack ? path.basename(pack) : null;
700
719
  return applyGate.ensureApply({
701
720
  cwd,
702
721
  source: url,
703
722
  rel: id ? applySidecarRel(id) : null,
704
723
  now,
705
724
  output,
706
- incompleteMessage: pack
707
- ? `next: apply ${pack}. keep only if measure.py moves 0→1`
725
+ incompleteMessage: slug
726
+ ? `next: atris experiments keep ${slug}`
708
727
  : APPLY_NEXT_MESSAGE,
709
728
  required: false,
710
729
  change: pack ? `apply ${pack}` : undefined,
@@ -815,6 +834,9 @@ function runYoutubeUnsave(args = [], deps = {}) {
815
834
  const status = unsaveYoutubeNotes(target, deps);
816
835
  if (status !== 0) code = status;
817
836
  }
837
+ if (code === 0 && parsed.json !== true && deps.json !== true) {
838
+ printWatchSearchNext(output);
839
+ }
818
840
  return code;
819
841
  }
820
842
 
@@ -977,6 +999,104 @@ function appendDigestJournal({ cwd, date, relDigest }) {
977
999
  fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
978
1000
  }
979
1001
 
1002
+ function digestExperimentSlug(date) {
1003
+ return `digest-${String(date || '').slice(0, 10)}`;
1004
+ }
1005
+
1006
+ function digestExperimentRel(date) {
1007
+ return `atris/experiments/${digestExperimentSlug(date)}`;
1008
+ }
1009
+
1010
+ function digestApplyRel(date) {
1011
+ return applyGate.applySidecarRel('digest', String(date || '').slice(0, 10));
1012
+ }
1013
+
1014
+ function ensureDigestApply({ cwd, date, packRel, now, output, source } = {}) {
1015
+ const stamp = String(date || '').slice(0, 10);
1016
+ const pack = packRel || (stamp ? digestExperimentRel(stamp) : null);
1017
+ const slug = pack ? path.basename(pack) : null;
1018
+ return applyGate.ensureApply({
1019
+ cwd,
1020
+ source: source || (stamp ? `digest:${stamp}` : 'digest'),
1021
+ rel: stamp ? digestApplyRel(stamp) : null,
1022
+ now,
1023
+ output,
1024
+ incompleteMessage: slug
1025
+ ? `next: atris experiments keep ${slug}`
1026
+ : applyGate.ephemeralApplyMessage('digest'),
1027
+ required: false,
1028
+ change: pack ? `apply ${pack}` : undefined,
1029
+ receipt: pack ? TEACH_KEEP_RULE : undefined,
1030
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
1031
+ });
1032
+ }
1033
+
1034
+ function saveRichDigest({ cwd, date, lesson, source } = {}) {
1035
+ if (isThinTeachLesson(lesson)) {
1036
+ return { thin: true, packRel: null, lesson };
1037
+ }
1038
+ const packRel = fileTeachExperiment({
1039
+ cwd,
1040
+ lesson,
1041
+ slug: date ? digestExperimentSlug(date) : null,
1042
+ applyRel: date ? digestApplyRel(date) : null,
1043
+ });
1044
+ return { thin: false, packRel, lesson, source };
1045
+ }
1046
+
1047
+ function watchExperimentSlug(id) {
1048
+ return `watch-${experimentIdToken(id)}`;
1049
+ }
1050
+
1051
+ function watchExperimentRel(id) {
1052
+ return `atris/experiments/${watchExperimentSlug(id)}`;
1053
+ }
1054
+
1055
+ function watchApplyRel(id) {
1056
+ return applyGate.applySidecarRel('watch', experimentIdToken(id));
1057
+ }
1058
+
1059
+ function ensureWatchWiki(cwd) {
1060
+ if (!cwd) return;
1061
+ fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
1062
+ }
1063
+
1064
+ function ensureWatchApply({ cwd, url, packRel, now, output, source } = {}) {
1065
+ const id = videoIdFromUrl(url);
1066
+ const pack = packRel || (id ? watchExperimentRel(id) : null);
1067
+ const slug = pack ? path.basename(pack) : null;
1068
+ ensureWatchWiki(cwd);
1069
+ return applyGate.ensureApply({
1070
+ cwd,
1071
+ source: source || url || (id ? `watch:${id}` : 'watch'),
1072
+ rel: id ? watchApplyRel(id) : null,
1073
+ now,
1074
+ output,
1075
+ incompleteMessage: slug
1076
+ ? `next: atris experiments keep ${slug}`
1077
+ : applyGate.ephemeralApplyMessage('watch'),
1078
+ required: false,
1079
+ change: pack ? `apply ${pack}` : undefined,
1080
+ receipt: pack ? TEACH_KEEP_RULE : undefined,
1081
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
1082
+ });
1083
+ }
1084
+
1085
+ function saveRichWatch({ cwd, url, lesson } = {}) {
1086
+ if (isThinTeachLesson(lesson)) {
1087
+ return { thin: true, packRel: null, lesson };
1088
+ }
1089
+ const id = videoIdFromUrl(url);
1090
+ const packRel = fileTeachExperiment({
1091
+ cwd,
1092
+ url,
1093
+ lesson,
1094
+ slug: id ? watchExperimentSlug(id) : null,
1095
+ applyRel: id ? watchApplyRel(id) : null,
1096
+ });
1097
+ return { thin: false, packRel, lesson, source: url };
1098
+ }
1099
+
980
1100
  function runYoutubeDigest(args = [], deps = {}) {
981
1101
  const output = deps.output || ((line = '') => console.log(line));
982
1102
  let options;
@@ -996,6 +1116,7 @@ function runYoutubeDigest(args = [], deps = {}) {
996
1116
  const briefs = collectVideoBriefs({ cwd, now, days: options.days });
997
1117
  if (!briefs.length) {
998
1118
  output(`no video briefs in the last ${options.days} days`);
1119
+ printWatchSearchNext(output);
999
1120
  return 0;
1000
1121
  }
1001
1122
 
@@ -1023,7 +1144,32 @@ function runYoutubeDigest(args = [], deps = {}) {
1023
1144
  fs.writeFileSync(path.join(cwd, relDigest), `${header}\n\n${text}\n`);
1024
1145
  appendDigestJournal({ cwd, date, relDigest });
1025
1146
  output(`digest filed: ${relDigest} (${briefs.length} briefs)`);
1026
- return 0;
1147
+ const lesson = notesLessonFromText(text);
1148
+ if (isThinTeachLesson(lesson)) {
1149
+ printLearnerCheckGate(output, lesson, { includeCheck: true });
1150
+ printWatchTickNext(output);
1151
+ return 0;
1152
+ }
1153
+
1154
+ const saved = saveRichDigest({ cwd, date, lesson, source: relDigest });
1155
+ const ensureApply = deps.ensureApply || ensureDigestApply;
1156
+ const applyCode = ensureApply({
1157
+ cwd,
1158
+ date,
1159
+ packRel: saved.packRel,
1160
+ now,
1161
+ output,
1162
+ source: relDigest,
1163
+ });
1164
+ if (deps.ensureApply) return applyCode;
1165
+ const baseline = proveSavedLearnerBaseline({
1166
+ cwd,
1167
+ applyRel: digestApplyRel(date),
1168
+ lesson,
1169
+ output,
1170
+ });
1171
+ if (baseline !== 0) return baseline;
1172
+ return applyCode;
1027
1173
  }
1028
1174
 
1029
1175
  function watchStatePath(cwd = process.cwd()) {
@@ -1172,6 +1318,22 @@ function markSeen(state, channel, id, timestamp) {
1172
1318
  state.seenByChannel[channel][id] = timestamp;
1173
1319
  }
1174
1320
 
1321
+ const WATCH_TICK_NEXT = 'next: atris youtube watch tick';
1322
+ const WATCH_ADD_NEXT = 'next: atris youtube watch add <channel-url-or-@handle>';
1323
+ const WATCH_SEARCH_NEXT = 'next: atris youtube search " "';
1324
+
1325
+ function printWatchTickNext(output) {
1326
+ output(WATCH_TICK_NEXT);
1327
+ }
1328
+
1329
+ function printWatchAddNext(output) {
1330
+ output(WATCH_ADD_NEXT);
1331
+ }
1332
+
1333
+ function printWatchSearchNext(output) {
1334
+ output(WATCH_SEARCH_NEXT);
1335
+ }
1336
+
1175
1337
  function addWatchChannel(channelInput, deps = {}) {
1176
1338
  const output = deps.output || ((line = '') => console.log(line));
1177
1339
  if (!channelInput) {
@@ -1191,6 +1353,7 @@ function addWatchChannel(channelInput, deps = {}) {
1191
1353
  const state = loadWatchState(statePath);
1192
1354
  if (state.channels.some((row) => row.channel === channel)) {
1193
1355
  output(`already watching ${channel}`);
1356
+ printWatchTickNext(output);
1194
1357
  return 0;
1195
1358
  }
1196
1359
 
@@ -1200,6 +1363,7 @@ function addWatchChannel(channelInput, deps = {}) {
1200
1363
  });
1201
1364
  saveWatchState(statePath, state);
1202
1365
  output(`watching ${channel}`);
1366
+ printWatchTickNext(output);
1203
1367
  return 0;
1204
1368
  }
1205
1369
 
@@ -1208,6 +1372,7 @@ function listWatchChannels(deps = {}) {
1208
1372
  const state = loadWatchState(resolveWatchStatePath(deps));
1209
1373
  if (!state.channels.length) {
1210
1374
  output('no channels watched');
1375
+ printWatchAddNext(output);
1211
1376
  return 0;
1212
1377
  }
1213
1378
 
@@ -1215,6 +1380,7 @@ function listWatchChannels(deps = {}) {
1215
1380
  const count = Object.keys(channelSeenMap(state, row.channel)).length;
1216
1381
  output(`${index + 1}. ${row.channel} (${count} seen)`);
1217
1382
  });
1383
+ printWatchTickNext(output);
1218
1384
  return 0;
1219
1385
  }
1220
1386
 
@@ -1231,6 +1397,11 @@ function removeWatchChannel(rawNumber, deps = {}) {
1231
1397
  const [removed] = state.channels.splice(index - 1, 1);
1232
1398
  saveWatchState(statePath, state);
1233
1399
  output(`removed ${removed.channel}`);
1400
+ if (!state.channels.length) {
1401
+ printWatchAddNext(output);
1402
+ } else {
1403
+ printWatchTickNext(output);
1404
+ }
1234
1405
  return 0;
1235
1406
  }
1236
1407
 
@@ -1248,6 +1419,7 @@ async function tickWatch(deps = {}) {
1248
1419
  const state = loadWatchState(statePath);
1249
1420
  let totalNew = 0;
1250
1421
  let totalBriefed = 0;
1422
+ let firstBriefedUrl = null;
1251
1423
 
1252
1424
  for (const row of state.channels) {
1253
1425
  const videosUrl = channelVideosUrl(row.channel);
@@ -1294,6 +1466,7 @@ async function tickWatch(deps = {}) {
1294
1466
  }
1295
1467
  markSeen(state, row.channel, video.id, timestamp);
1296
1468
  briefed += 1;
1469
+ if (!firstBriefedUrl) firstBriefedUrl = url;
1297
1470
  }
1298
1471
 
1299
1472
  if (!isFresh) {
@@ -1311,6 +1484,40 @@ async function tickWatch(deps = {}) {
1311
1484
 
1312
1485
  output(`total: ${totalNew} new, ${totalBriefed} briefed`);
1313
1486
  saveWatchState(statePath, state);
1487
+ if (totalBriefed > 0) {
1488
+ if (firstBriefedUrl) {
1489
+ const lesson = notesLessonFromText(readNotesText({ url: firstBriefedUrl, workDir }));
1490
+ if (isThinTeachLesson(lesson)) {
1491
+ printLearnerCheckGate(output, lesson, { includeCheck: true });
1492
+ printYoutubeTeachNext(firstBriefedUrl, {}, output);
1493
+ return 0;
1494
+ }
1495
+ const saved = saveRichWatch({ cwd, url: firstBriefedUrl, lesson });
1496
+ const ensureApply = deps.ensureApply || ensureWatchApply;
1497
+ const applyCode = ensureApply({
1498
+ cwd,
1499
+ url: firstBriefedUrl,
1500
+ packRel: saved.packRel,
1501
+ now,
1502
+ output,
1503
+ source: firstBriefedUrl,
1504
+ });
1505
+ if (deps.ensureApply) return applyCode;
1506
+ const id = videoIdFromUrl(firstBriefedUrl);
1507
+ const baseline = proveSavedLearnerBaseline({
1508
+ cwd,
1509
+ applyRel: id ? watchApplyRel(id) : null,
1510
+ lesson,
1511
+ output,
1512
+ });
1513
+ if (baseline !== 0) return baseline;
1514
+ return applyCode;
1515
+ }
1516
+ printYoutubeTeachNext(firstBriefedUrl, {}, output);
1517
+ return 0;
1518
+ }
1519
+ if (!state.channels.length) printWatchAddNext(output);
1520
+ else printWatchSearchNext(output);
1314
1521
  return 0;
1315
1522
  }
1316
1523
 
@@ -1447,6 +1654,7 @@ function runOneNotesItem(item, engine, deps = {}) {
1447
1654
  status = 1;
1448
1655
  }
1449
1656
  let brief = null;
1657
+ let lesson = null;
1450
1658
  let ok = status === 0;
1451
1659
  if (ok && deps.save) {
1452
1660
  const saved = saveRichNotes(item.url, deps);
@@ -1455,6 +1663,7 @@ function runOneNotesItem(item, engine, deps = {}) {
1455
1663
  ok = false;
1456
1664
  } else {
1457
1665
  brief = saved.brief;
1666
+ lesson = saved.lesson;
1458
1667
  const ensureApply = deps.ensureApply || ensureNotesApply;
1459
1668
  try {
1460
1669
  ensureApply({
@@ -1471,7 +1680,7 @@ function runOneNotesItem(item, engine, deps = {}) {
1471
1680
  }
1472
1681
  const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
1473
1682
  output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
1474
- return { url: item.url, id: item.id, seconds, ok, brief };
1683
+ return { url: item.url, id: item.id, seconds, ok, brief, lesson };
1475
1684
  }
1476
1685
 
1477
1686
  function formatNotesSummary(rows = []) {
@@ -1483,7 +1692,14 @@ function formatNotesSummary(rows = []) {
1483
1692
  return lines.join('\n');
1484
1693
  }
1485
1694
 
1486
- function runYoutubeNotesBatch({ urls, engine, save } = {}, deps = {}) {
1695
+ function printEphemeralNotesLearnerGate(url, { json = false, workDir } = {}, output) {
1696
+ const dir = workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
1697
+ const lesson = notesLessonFromText(readNotesText({ url, workDir: dir }));
1698
+ if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'notes');
1699
+ printLearnerCheckGate(output, lesson, { includeCheck: true, json });
1700
+ }
1701
+
1702
+ function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
1487
1703
  deps = { ...deps, save: save === true || deps.save === true };
1488
1704
  const output = deps.output || ((line = '') => console.error(line));
1489
1705
  const items = expandNotesTargets(urls || [], deps);
@@ -1496,7 +1712,30 @@ function runYoutubeNotesBatch({ urls, engine, save } = {}, deps = {}) {
1496
1712
  output(formatNotesSummary(rows));
1497
1713
  }
1498
1714
  if (!rows.length) return 2;
1499
- return rows.some((row) => row.ok) ? 0 : 2;
1715
+ const firstOk = rows.find((row) => row.ok);
1716
+ const asJson = json === true || deps.json === true;
1717
+ if (firstOk && !deps.save) {
1718
+ if (!asJson) printEphemeralNotesLearnerGate(firstOk.url, { workDir: deps.workDir }, output);
1719
+ printYoutubeTeachNext(firstOk.url, { json: asJson }, output);
1720
+ }
1721
+ if (firstOk && deps.save) {
1722
+ if (deps.ensureApply) return 0;
1723
+ const id = firstOk.id || videoIdFromUrl(firstOk.url);
1724
+ const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
1725
+ const lesson = firstOk.lesson || notesLessonFromText(readNotesText({
1726
+ url: firstOk.url,
1727
+ workDir,
1728
+ }));
1729
+ const baseline = proveSavedLearnerBaseline({
1730
+ cwd: deps.cwd || process.cwd(),
1731
+ applyRel: id ? applySidecarRel(id) : null,
1732
+ lesson,
1733
+ output,
1734
+ json: asJson,
1735
+ });
1736
+ if (baseline !== 0) return baseline;
1737
+ }
1738
+ return firstOk ? 0 : 2;
1500
1739
  }
1501
1740
 
1502
1741
  function runSingleYoutubeNotes(url, engine, deps = {}) {
@@ -1510,24 +1749,37 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
1510
1749
  if (status !== 0) return status;
1511
1750
  const output = deps.output || ((line = '') => console.error(line));
1512
1751
  if (!deps.save) {
1513
- const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
1514
- const lesson = notesLessonFromText(readNotesText({ url, workDir }));
1515
- if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'notes');
1752
+ const json = deps.json === true;
1753
+ printEphemeralNotesLearnerGate(url, { json, workDir: deps.workDir }, output);
1754
+ printYoutubeTeachNext(url, { json }, output);
1516
1755
  return 0;
1517
1756
  }
1518
1757
  const saved = saveRichNotes(url, deps);
1519
1758
  if (saved.thin) {
1520
1759
  output(TEACH_THIN_REFUSE);
1760
+ printYoutubeTeachNext(url, { json: deps.json }, output);
1521
1761
  return 2;
1522
1762
  }
1523
1763
  const ensureApply = deps.ensureApply || ensureNotesApply;
1524
- return ensureApply({
1525
- cwd: deps.cwd || process.cwd(),
1764
+ const cwd = deps.cwd || process.cwd();
1765
+ const applyCode = ensureApply({
1766
+ cwd,
1526
1767
  url,
1527
1768
  packRel: saved.packRel,
1528
1769
  now: deps.now,
1529
1770
  output: deps.output,
1530
1771
  });
1772
+ if (deps.ensureApply) return applyCode;
1773
+ const id = videoIdFromUrl(url);
1774
+ const baseline = proveSavedLearnerBaseline({
1775
+ cwd,
1776
+ applyRel: id ? applySidecarRel(id) : null,
1777
+ lesson: saved.lesson,
1778
+ output,
1779
+ json: deps.json === true,
1780
+ });
1781
+ if (baseline !== 0) return baseline;
1782
+ return applyCode;
1531
1783
  }
1532
1784
 
1533
1785
  function runYoutubeNotes(args = [], deps = {}) {
@@ -1545,7 +1797,7 @@ function runYoutubeNotes(args = [], deps = {}) {
1545
1797
  output(YTNOTES_HINT);
1546
1798
  return 2;
1547
1799
  }
1548
- const nextDeps = { ...deps, save: parsed.save };
1800
+ const nextDeps = { ...deps, save: parsed.save, json: parsed.json };
1549
1801
  if (parsed.urls.length === 1 && !isPlaylistUrl(parsed.urls[0])) {
1550
1802
  return runSingleYoutubeNotes(parsed.urls[0], parsed.engine, nextDeps);
1551
1803
  }
@@ -1575,10 +1827,13 @@ function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtub
1575
1827
  output('');
1576
1828
  output('Free local discovery. Uses ytsearch on PATH when present, else the');
1577
1829
  output('bundled scripts/det/ytsearch, else yt-dlp ytsearchN with the same print contract.');
1578
- output('Does not bill credits. Process stays the 5-credit step after you pick a URL.');
1830
+ output('Does not bill credits. A hit prints one next: atris youtube teach <first-url>.');
1831
+ output('A rich hit prints one failing check (score 0). A thin hit prints check: fill this.');
1579
1832
  output('');
1580
1833
  output(`--paid buys watch permalinks from Atris (${PAID_SEARCH_COST_HINT}).`);
1581
1834
  output('Requires login. Same auth path as atris youtube process.');
1835
+ output('A hit also prints one next: atris youtube teach <first-url>.');
1836
+ output('A rich hit prints one failing check (score 0). A thin hit prints check: fill this.');
1582
1837
  output('Empty or failed paid search refunds the credits.');
1583
1838
  output('');
1584
1839
  output('Options:');
@@ -1709,6 +1964,17 @@ function resolveSearchCachePath(deps = {}) {
1709
1964
  return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
1710
1965
  }
1711
1966
 
1967
+ function printYoutubeTeachNext(url, options, output) {
1968
+ if (options && options.json) return;
1969
+ if (!url) return;
1970
+ output(`next: atris youtube teach ${quoteYoutubeUrl(url)}`);
1971
+ }
1972
+
1973
+ function printSearchTeachNext(rows, options, output) {
1974
+ const firstUrl = Array.isArray(rows) && rows[0] && rows[0].url;
1975
+ printYoutubeTeachNext(firstUrl, options, output);
1976
+ }
1977
+
1712
1978
  function printSearchRows(rows, options, output) {
1713
1979
  if (options.json) {
1714
1980
  output(JSON.stringify(rows, null, 2));
@@ -1908,12 +2174,29 @@ async function runPaidYoutubeSearch(options, deps = {}) {
1908
2174
  const rendered = formatPaidSearchResults(data);
1909
2175
  if (!videos.length) {
1910
2176
  output(rendered ? `no videos found\n${rendered}` : 'no videos found');
2177
+ printWatchTickNext(output);
1911
2178
  return 2;
1912
2179
  }
1913
2180
  output(rendered);
2181
+ printSearchLearnerGate(videos, options, output);
2182
+ printSearchTeachNext(videos, options, output);
1914
2183
  return 0;
1915
2184
  }
1916
2185
 
2186
+ function searchLessonText(rows) {
2187
+ return (Array.isArray(rows) ? rows : [])
2188
+ .map((row) => String(row && row.title ? row.title : '').trim())
2189
+ .filter(Boolean)
2190
+ .join('\n');
2191
+ }
2192
+
2193
+ function printSearchLearnerGate(rows, options, output) {
2194
+ printLearnerCheckGate(output, notesLessonFromText(searchLessonText(rows)), {
2195
+ includeCheck: true,
2196
+ json: Boolean(options && options.json),
2197
+ });
2198
+ }
2199
+
1917
2200
  function commandOnPath(name, deps = {}) {
1918
2201
  const spawn = deps.spawnSync || spawnSync;
1919
2202
  const result = spawn('sh', ['-c', `command -v ${shellSingleQuote(name)}`], {
@@ -2049,6 +2332,7 @@ async function runYoutubeSearch(args = [], deps = {}) {
2049
2332
  if (cached) {
2050
2333
  const rows = cached.rows.slice(0, options.limit);
2051
2334
  printSearchRows(rows, options, output);
2335
+ printSearchTeachNext(rows, options, output);
2052
2336
  output(LOCAL_SEARCH_CACHE_NOTE);
2053
2337
  return 0;
2054
2338
  }
@@ -2067,19 +2351,26 @@ async function runYoutubeSearch(args = [], deps = {}) {
2067
2351
  const rows = parseSearchStdout(stdout);
2068
2352
  if (!rows.length) {
2069
2353
  output('no videos found');
2354
+ if (!options.json) printWatchTickNext(output);
2070
2355
  return 2;
2071
2356
  }
2072
2357
 
2073
2358
  writeLocalSearchCache(options.query, rows, deps);
2074
2359
  printSearchRows(rows, options, output);
2360
+ printSearchLearnerGate(rows, options, output);
2361
+ printSearchTeachNext(rows, options, output);
2075
2362
  return 0;
2076
2363
  }
2077
2364
 
2078
- const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]';
2365
+ const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip] | owed | next';
2079
2366
  const TEACH_PAID_REFUSE = 'teach is free local captions. drop --paid.';
2080
2367
  const TEACH_THIN_REFUSE = 'thin: no number or named mechanism. no brief.';
2081
2368
  const TEACH_OWED_FILE = 'youtube-teach-owed.json';
2082
2369
  const TEACH_RECAP_MISSING = '--recap needs the unpaid check';
2370
+ const TEACH_RESUME_START = 'atris youtube teach <url>';
2371
+ const TEACH_RESUME_NEXT = 'next: atris youtube teach recap TEXT or atris youtube teach skip';
2372
+ const TEACH_WATCH_TICK_NEXT = WATCH_TICK_NEXT;
2373
+ const TEACH_CONTINUE_NEXT = 'next: atris youtube teach next';
2083
2374
  const TEACH_APPLY_NEXT_MESSAGE = APPLY_NEXT_MESSAGE;
2084
2375
  const TEACH_KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
2085
2376
  const MECHANISM_STOP = new Set([
@@ -2108,12 +2399,16 @@ function parseTeachArgs(argv = []) {
2108
2399
  save: false,
2109
2400
  json: false,
2110
2401
  skip: false,
2402
+ owed: false,
2403
+ resume: false,
2404
+ next: false,
2111
2405
  recap: null,
2112
2406
  url: null,
2113
2407
  section: 1,
2114
2408
  };
2409
+ let sectionSet = false;
2115
2410
 
2116
- if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
2411
+ if (args.length > 0 && ['help', '--help', '-h'].includes(args[0])) {
2117
2412
  options.help = true;
2118
2413
  return options;
2119
2414
  }
@@ -2128,6 +2423,8 @@ function parseTeachArgs(argv = []) {
2128
2423
  options.json = true;
2129
2424
  } else if (arg === '--skip') {
2130
2425
  options.skip = true;
2426
+ } else if (arg === '--owed') {
2427
+ options.owed = true;
2131
2428
  } else if (arg === '--paid') {
2132
2429
  throw new Error(TEACH_PAID_REFUSE);
2133
2430
  } else if (arg === '--recap') {
@@ -2148,6 +2445,7 @@ function parseTeachArgs(argv = []) {
2148
2445
  throw new Error('--section must be a positive integer');
2149
2446
  }
2150
2447
  options.section = value;
2448
+ sectionSet = true;
2151
2449
  i += 1;
2152
2450
  } else if (arg.startsWith('--section=')) {
2153
2451
  const value = Number.parseInt(arg.slice('--section='.length), 10);
@@ -2155,6 +2453,7 @@ function parseTeachArgs(argv = []) {
2155
2453
  throw new Error('--section must be a positive integer');
2156
2454
  }
2157
2455
  options.section = value;
2456
+ sectionSet = true;
2158
2457
  } else if (arg.startsWith('-')) {
2159
2458
  throw new Error(`Unknown option: ${arg}`);
2160
2459
  } else if (arg === 'recap' && options.recap == null && !options.url) {
@@ -2169,6 +2468,10 @@ function parseTeachArgs(argv = []) {
2169
2468
  if (!options.recap) throw new Error(TEACH_RECAP_MISSING);
2170
2469
  } else if (arg === 'skip' && !options.url) {
2171
2470
  options.skip = true;
2471
+ } else if (arg === 'owed' && !options.url) {
2472
+ options.owed = true;
2473
+ } else if (arg === 'next' && !options.url) {
2474
+ options.next = true;
2172
2475
  } else if (!options.url && looksLikeYoutubeUrl(arg)) {
2173
2476
  options.url = arg;
2174
2477
  } else {
@@ -2177,7 +2480,12 @@ function parseTeachArgs(argv = []) {
2177
2480
  }
2178
2481
 
2179
2482
  if (options.help) return options;
2180
- if (!options.url && options.recap == null && !options.skip) {
2483
+ if (!options.url && options.recap == null && !options.skip && !options.owed && !options.next) {
2484
+ if (!sectionSet && !options.save) {
2485
+ options.owed = true;
2486
+ options.resume = true;
2487
+ return options;
2488
+ }
2181
2489
  throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
2182
2490
  }
2183
2491
  return options;
@@ -2297,7 +2605,13 @@ function extractTeachMechanisms(text) {
2297
2605
  return found.slice(0, 8);
2298
2606
  }
2299
2607
 
2300
- function oneTeachCheck(mechanisms, numbers, title) {
2608
+ const LEARNER_CHECK_FILL = 'fill this';
2609
+ const LEARNER_SCORE_ZERO = 'score: 0';
2610
+ const LEARNER_CHECK_INCOMPLETE = 'incomplete: no measurable check. check: fill this';
2611
+ const LEARNER_BASELINE_INCOMPLETE = 'incomplete: check already passes. refuse invented success.';
2612
+ const LEARNER_APPLY_MISSING = 'incomplete: apply missing. check cannot score.';
2613
+
2614
+ function oneTeachCheck(mechanisms, numbers) {
2301
2615
  if (mechanisms[0]) {
2302
2616
  const name = mechanisms[0];
2303
2617
  const named = new RegExp(`\\b(?:${MECHANISM_HEADS})\\b`, 'i').test(name);
@@ -2305,7 +2619,63 @@ function oneTeachCheck(mechanisms, numbers, title) {
2305
2619
  return `what is ${name}?`;
2306
2620
  }
2307
2621
  if (numbers[0]) return `what does ${numbers[0]} measure in this chapter?`;
2308
- return `what is the point of ${title || 'this chapter'}?`;
2622
+ return LEARNER_CHECK_FILL;
2623
+ }
2624
+
2625
+ function learnerCheckFromLesson(lesson = {}) {
2626
+ const mechanisms = Array.isArray(lesson.mechanisms) ? lesson.mechanisms : [];
2627
+ const numbers = Array.isArray(lesson.numbers) ? lesson.numbers : [];
2628
+ const inferred = mechanisms.length > 0 || numbers.length > 0;
2629
+ return {
2630
+ inferred,
2631
+ line: inferred ? oneTeachCheck(mechanisms, numbers) : LEARNER_CHECK_FILL,
2632
+ needles: teachCheckNeedles({ mechanisms, numbers }),
2633
+ };
2634
+ }
2635
+
2636
+ function scoreLearnerNeedles(text, needles) {
2637
+ const list = Array.isArray(needles) ? needles : [];
2638
+ if (!list.length) return 0;
2639
+ const blob = String(text || '').toLowerCase();
2640
+ return list.every((needle) => blob.includes(String(needle || '').toLowerCase())) ? 1 : 0;
2641
+ }
2642
+
2643
+ function printLearnerCheckGate(output, lesson, { includeCheck = false, json = false } = {}) {
2644
+ if (json || typeof output !== 'function') return;
2645
+ const check = learnerCheckFromLesson(lesson);
2646
+ if (includeCheck) output(`check: ${check.line}`);
2647
+ if (check.inferred) output(LEARNER_SCORE_ZERO);
2648
+ }
2649
+
2650
+ function proveSavedLearnerBaseline({ cwd, applyRel, lesson, output, json } = {}) {
2651
+ const print = typeof output === 'function' ? output : () => {};
2652
+ const check = learnerCheckFromLesson(lesson);
2653
+ if (!check.inferred) {
2654
+ if (!json) print(LEARNER_CHECK_INCOMPLETE);
2655
+ return 2;
2656
+ }
2657
+ if (!cwd || !applyRel) {
2658
+ if (!json) print(LEARNER_APPLY_MISSING);
2659
+ return 2;
2660
+ }
2661
+ const abs = path.join(cwd, applyRel);
2662
+ let text = '';
2663
+ try {
2664
+ if (!fs.existsSync(abs)) {
2665
+ if (!json) print(LEARNER_APPLY_MISSING);
2666
+ return 2;
2667
+ }
2668
+ text = fs.readFileSync(abs, 'utf8');
2669
+ } catch {
2670
+ if (!json) print(LEARNER_APPLY_MISSING);
2671
+ return 2;
2672
+ }
2673
+ if (scoreLearnerNeedles(text, check.needles) !== 0) {
2674
+ if (!json) print(LEARNER_BASELINE_INCOMPLETE);
2675
+ return 2;
2676
+ }
2677
+ if (!json) print(LEARNER_SCORE_ZERO);
2678
+ return 0;
2309
2679
  }
2310
2680
 
2311
2681
  function quoteYoutubeUrl(url) {
@@ -2336,10 +2706,7 @@ function teachLessonFromCues({ url, section, chapters, chapter, cues, title } =
2336
2706
  lines.push(oneTeachCheck(mechanisms, numbers, heading));
2337
2707
  if (section < total) {
2338
2708
  lines.push('');
2339
- lines.push(`next: atris youtube teach ${quoteYoutubeUrl(url)} --section ${section + 1}`);
2340
- } else {
2341
- lines.push('');
2342
- lines.push('next: last section');
2709
+ lines.push(TEACH_RESUME_NEXT);
2343
2710
  }
2344
2711
  return { text: lines.join('\n'), numbers, mechanisms };
2345
2712
  }
@@ -2429,32 +2796,136 @@ function writeTeachOwedStore(deps, store) {
2429
2796
  }
2430
2797
  }
2431
2798
 
2432
- function rememberTeachOwed(deps, { url, section, lesson } = {}) {
2799
+ function rememberTeachOwed(deps, { url, section, lesson, total } = {}) {
2433
2800
  const id = videoIdFromUrl(url);
2434
2801
  if (!id) return;
2435
2802
  const store = readTeachOwedStore(deps);
2436
- store[id] = {
2803
+ const row = {
2437
2804
  section: Number(section) || 1,
2438
2805
  check: teachCheckLine(lesson),
2439
2806
  tokens: teachRecapTokens(lesson),
2807
+ url: url || undefined,
2440
2808
  };
2809
+ const totalChapters = Number(total);
2810
+ if (Number.isInteger(totalChapters) && totalChapters > 0) row.total = totalChapters;
2811
+ store[id] = row;
2441
2812
  writeTeachOwedStore(deps, store);
2442
2813
  }
2443
2814
 
2815
+ function writeTeachContinue(store, id, entry) {
2816
+ const nextSection = (Number(entry && entry.section) || 1) + 1;
2817
+ const total = Number(entry && entry.total);
2818
+ if (Number.isInteger(total) && total > 0 && nextSection > total) {
2819
+ delete store[id];
2820
+ return null;
2821
+ }
2822
+ const cursor = {
2823
+ section: nextSection,
2824
+ url: (entry && entry.url) || undefined,
2825
+ };
2826
+ if (Number.isInteger(total) && total > 0) cursor.total = total;
2827
+ store[id] = cursor;
2828
+ return cursor;
2829
+ }
2830
+
2831
+ function lastTeachContinueEntry(store) {
2832
+ const ids = Object.keys(store || {});
2833
+ for (let i = ids.length - 1; i >= 0; i -= 1) {
2834
+ const entry = store[ids[i]];
2835
+ if (!entry || typeof entry !== 'object') continue;
2836
+ if (String(entry.check || '').trim()) continue;
2837
+ const url = String(entry.url || '').trim();
2838
+ if (!url) continue;
2839
+ return {
2840
+ id: ids[i],
2841
+ url,
2842
+ section: Number(entry.section) || 1,
2843
+ };
2844
+ }
2845
+ return null;
2846
+ }
2847
+
2848
+ function lastTeachUnpaidEntry(store) {
2849
+ const ids = Object.keys(store || {});
2850
+ for (let i = ids.length - 1; i >= 0; i -= 1) {
2851
+ const entry = store[ids[i]];
2852
+ if (!entry || typeof entry !== 'object') continue;
2853
+ const check = String(entry.check || '').trim();
2854
+ if (!check) continue;
2855
+ return {
2856
+ id: ids[i],
2857
+ check,
2858
+ url: entry.url || null,
2859
+ section: Number(entry.section) || 1,
2860
+ };
2861
+ }
2862
+ return null;
2863
+ }
2864
+
2444
2865
  function previousTeachUnlocked(owed, section) {
2445
2866
  if (!owed) return true;
2446
2867
  return Number(owed.section) >= Number(section);
2447
2868
  }
2448
2869
 
2870
+ function printTeachUnlockNext(parsed, entry, output) {
2871
+ if (!parsed || parsed.json) return;
2872
+ const url = parsed.url || (entry && entry.url) || '';
2873
+ if (!url) return;
2874
+ output(TEACH_CONTINUE_NEXT);
2875
+ }
2876
+
2877
+ function printTeachWatchTickNext(parsed, lesson, total, output) {
2878
+ if (!parsed || parsed.json) return;
2879
+ if (parsed.save && !isThinTeachLesson(lesson)) return;
2880
+ const section = Number(parsed.section) || 0;
2881
+ if (section < Number(total)) return;
2882
+ printWatchTickNext(output);
2883
+ }
2884
+
2885
+ function listTeachOwedEntries(store) {
2886
+ return Object.keys(store || {}).map((id) => {
2887
+ const entry = store[id] && typeof store[id] === 'object' ? store[id] : {};
2888
+ return {
2889
+ id,
2890
+ url: entry.url || null,
2891
+ section: Number(entry.section) || 1,
2892
+ check: String(entry.check || '').trim(),
2893
+ };
2894
+ }).filter((row) => row.check);
2895
+ }
2896
+
2897
+ function printTeachOwed(parsed, deps, output) {
2898
+ const rows = listTeachOwedEntries(readTeachOwedStore(deps));
2899
+ if (parsed.json) {
2900
+ output(JSON.stringify(rows));
2901
+ return 0;
2902
+ }
2903
+ if (!rows.length) {
2904
+ output('nothing owed');
2905
+ if (parsed.resume) output(TEACH_RESUME_START);
2906
+ return 0;
2907
+ }
2908
+ rows.forEach((row, index) => {
2909
+ if (index) output('');
2910
+ output(row.url || row.id);
2911
+ output(`section ${row.section}`);
2912
+ if (row.check) output(row.check);
2913
+ });
2914
+ if (parsed.resume) output(TEACH_RESUME_NEXT);
2915
+ return 0;
2916
+ }
2917
+
2449
2918
  function applyTeachRecap(parsed, deps, output) {
2450
2919
  const store = readTeachOwedStore(deps);
2451
2920
  let id = parsed.url ? videoIdFromUrl(parsed.url) : null;
2452
2921
  let entry = id ? store[id] : null;
2453
2922
  if (!entry && !id) {
2454
2923
  const ids = Object.keys(store);
2924
+ const unpaidIds = ids.filter((key) => store[key] && String(store[key].check || '').trim());
2455
2925
  if (parsed.recap) {
2456
- id = ids.find((key) => recapHitsTokens(parsed.recap, store[key] && store[key].tokens));
2926
+ id = unpaidIds.find((key) => recapHitsTokens(parsed.recap, store[key] && store[key].tokens));
2457
2927
  }
2928
+ if (!id && unpaidIds.length === 1) id = unpaidIds[0];
2458
2929
  if (!id && ids.length === 1) id = ids[0];
2459
2930
  entry = id ? store[id] : null;
2460
2931
  }
@@ -2463,8 +2934,13 @@ function applyTeachRecap(parsed, deps, output) {
2463
2934
  if (!parsed.json) output('recap the previous section first');
2464
2935
  return 2;
2465
2936
  }
2937
+ if (!String(entry.check || '').trim()) {
2938
+ printTeachUnlockNext(parsed, entry, output);
2939
+ return 0;
2940
+ }
2466
2941
  if (parsed.skip) {
2467
- delete store[id];
2942
+ printTeachUnlockNext(parsed, entry, output);
2943
+ writeTeachContinue(store, id, entry);
2468
2944
  writeTeachOwedStore(deps, store);
2469
2945
  return 0;
2470
2946
  }
@@ -2472,11 +2948,40 @@ function applyTeachRecap(parsed, deps, output) {
2472
2948
  if (!parsed.json) output(entry.check);
2473
2949
  return 2;
2474
2950
  }
2475
- delete store[id];
2951
+ printTeachUnlockNext(parsed, entry, output);
2952
+ writeTeachContinue(store, id, entry);
2476
2953
  writeTeachOwedStore(deps, store);
2477
2954
  return 0;
2478
2955
  }
2479
2956
 
2957
+ function applyTeachNext(parsed, deps, output) {
2958
+ const store = readTeachOwedStore(deps);
2959
+ const cont = lastTeachContinueEntry(store);
2960
+ if (cont) {
2961
+ parsed.url = cont.url;
2962
+ parsed.section = cont.section;
2963
+ return 0;
2964
+ }
2965
+ const unpaid = lastTeachUnpaidEntry(store);
2966
+ if (unpaid) {
2967
+ if (!parsed.json) output(unpaid.check);
2968
+ return 2;
2969
+ }
2970
+ if (parsed.json) {
2971
+ output('[]');
2972
+ return 0;
2973
+ }
2974
+ output('nothing owed');
2975
+ output(TEACH_RESUME_START);
2976
+ return 0;
2977
+ }
2978
+
2979
+ function applyTeachResume(parsed, deps, output) {
2980
+ const store = readTeachOwedStore(deps);
2981
+ if (lastTeachUnpaidEntry(store)) return printTeachOwed(parsed, deps, output);
2982
+ return applyTeachNext(parsed, deps, output);
2983
+ }
2984
+
2480
2985
  function teachCheckLine(lesson = {}) {
2481
2986
  return oneTeachCheck(lesson.mechanisms || [], lesson.numbers || [], '');
2482
2987
  }
@@ -2765,14 +3270,15 @@ function fileTeachBrief({ cwd, url, section, lesson, now } = {}) {
2765
3270
  function ensureTeachApply({ cwd, url, section, packRel, now, output } = {}) {
2766
3271
  const id = videoIdFromUrl(url);
2767
3272
  const pack = packRel || (id ? teachExperimentRel(id, section) : null);
3273
+ const slug = pack ? path.basename(pack) : null;
2768
3274
  return applyGate.ensureApply({
2769
3275
  cwd,
2770
3276
  source: url,
2771
3277
  rel: id ? applySidecarRel(`${id}-s${section}`) : null,
2772
3278
  now,
2773
3279
  output,
2774
- incompleteMessage: pack
2775
- ? `next: apply ${pack}. keep only if measure.py moves 0→1`
3280
+ incompleteMessage: slug
3281
+ ? `next: atris experiments keep ${slug}`
2776
3282
  : TEACH_APPLY_NEXT_MESSAGE,
2777
3283
  required: false,
2778
3284
  change: pack ? `apply ${pack}` : undefined,
@@ -2832,11 +3338,22 @@ async function runYoutubeTeach(args = [], deps = {}) {
2832
3338
  }
2833
3339
 
2834
3340
  const owedDeps = { ...deps, cwd: deps.cwd || process.cwd() };
3341
+ if (parsed.owed) {
3342
+ if (!parsed.resume) return printTeachOwed(parsed, owedDeps, output);
3343
+ const resumeCode = applyTeachResume(parsed, owedDeps, output);
3344
+ if (resumeCode !== 0) return resumeCode;
3345
+ if (!parsed.url) return 0;
3346
+ }
2835
3347
  if (parsed.recap != null || parsed.skip) {
2836
3348
  const recapCode = applyTeachRecap(parsed, owedDeps, output);
2837
3349
  if (recapCode !== 0) return recapCode;
2838
3350
  if (!parsed.url || parsed.section <= 1) return 0;
2839
3351
  }
3352
+ if (parsed.next) {
3353
+ const nextCode = applyTeachNext(parsed, owedDeps, output);
3354
+ if (nextCode !== 0) return nextCode;
3355
+ if (!parsed.url) return 0;
3356
+ }
2840
3357
 
2841
3358
  if (!parsed.url) {
2842
3359
  output('Missing YouTube URL. Run "atris youtube teach --help".');
@@ -2874,39 +3391,59 @@ async function runYoutubeTeach(args = [], deps = {}) {
2874
3391
  title: source.title,
2875
3392
  });
2876
3393
  output(lesson.text);
2877
- rememberTeachOwed(owedDeps, { url: parsed.url, section: parsed.section, lesson });
3394
+ rememberTeachOwed(owedDeps, {
3395
+ url: parsed.url,
3396
+ section: parsed.section,
3397
+ lesson,
3398
+ total: chapters.length,
3399
+ });
2878
3400
 
2879
3401
  if (!parsed.save) {
2880
3402
  if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'teach');
3403
+ printLearnerCheckGate(output, lesson, { includeCheck: true, json: parsed.json === true });
3404
+ printTeachWatchTickNext(parsed, lesson, chapters.length, output);
2881
3405
  return 0;
2882
3406
  }
2883
3407
  if (isThinTeachLesson(lesson)) {
2884
3408
  output(TEACH_THIN_REFUSE);
3409
+ printTeachWatchTickNext(parsed, lesson, chapters.length, output);
2885
3410
  return 2;
2886
3411
  }
2887
3412
 
3413
+ const cwd = deps.cwd || process.cwd();
2888
3414
  fileTeachBrief({
2889
- cwd: deps.cwd || process.cwd(),
3415
+ cwd,
2890
3416
  url: parsed.url,
2891
3417
  section: parsed.section,
2892
3418
  lesson: lesson.text,
2893
3419
  now: deps.now,
2894
3420
  });
2895
3421
  const packRel = fileTeachExperiment({
2896
- cwd: deps.cwd || process.cwd(),
3422
+ cwd,
2897
3423
  url: parsed.url,
2898
3424
  section: parsed.section,
2899
3425
  lesson,
2900
3426
  });
2901
3427
  const ensureApply = deps.ensureApply || ensureTeachApply;
2902
- return ensureApply({
2903
- cwd: deps.cwd || process.cwd(),
3428
+ const applyCode = ensureApply({
3429
+ cwd,
2904
3430
  url: parsed.url,
2905
3431
  section: parsed.section,
2906
3432
  packRel,
2907
3433
  now: deps.now,
2908
3434
  output,
2909
3435
  });
3436
+ if (deps.ensureApply) return applyCode;
3437
+ const id = videoIdFromUrl(parsed.url);
3438
+ const baseline = proveSavedLearnerBaseline({
3439
+ cwd,
3440
+ applyRel: id ? applySidecarRel(`${id}-s${parsed.section}`) : null,
3441
+ lesson,
3442
+ output,
3443
+ json: parsed.json === true,
3444
+ });
3445
+ if (baseline !== 0) return baseline;
3446
+ return applyCode;
2910
3447
  }
2911
3448
 
2912
3449
  async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
@@ -2951,7 +3488,12 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
2951
3488
  let status = 0;
2952
3489
  try {
2953
3490
  const data = await processYoutube(options, deps);
2954
- output(options.json ? JSON.stringify(data, null, 2) : formatYoutubeResult(data));
3491
+ if (options.json) {
3492
+ output(JSON.stringify(data, null, 2));
3493
+ } else {
3494
+ output(formatYoutubeResult(data));
3495
+ printProcessLearnerGate(data, {}, output);
3496
+ }
2955
3497
  } catch (err) {
2956
3498
  if (!err.applyRequired) output(err.message);
2957
3499
  status = Number.isInteger(err.exitCode) ? err.exitCode : 1;
@@ -2999,10 +3541,21 @@ module.exports = {
2999
3541
  extractTeachNumbers,
3000
3542
  extractTeachMechanisms,
3001
3543
  extractTeachSource,
3544
+ oneTeachCheck,
3545
+ learnerCheckFromLesson,
3546
+ scoreLearnerNeedles,
3547
+ LEARNER_CHECK_FILL,
3548
+ LEARNER_SCORE_ZERO,
3549
+ printLearnerCheckGate,
3550
+ proveSavedLearnerBaseline,
3002
3551
  isThinTeachLesson,
3003
3552
  TEACH_THIN_REFUSE,
3553
+ TEACH_RESUME_NEXT,
3554
+ TEACH_WATCH_TICK_NEXT,
3004
3555
  teachExperimentSlug,
3005
3556
  notesExperimentSlug,
3557
+ digestExperimentSlug,
3558
+ watchExperimentSlug,
3006
3559
  fileTeachExperiment,
3007
3560
  youtubeCommand,
3008
3561
  };