atris 3.57.3 → 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 (175) 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 +57 -13
  6. package/atris/skills/x-search/SKILL.md +13 -5
  7. package/atris/skills/youtube/SKILL.md +39 -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 +157 -24
  16. package/bin/atris.js +79 -31
  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 +297 -15
  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 +97 -98
  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 +57 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +304 -15
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +916 -65
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/apply-gate.js +13 -0
  106. package/lib/auto-accept-certified.js +28 -12
  107. package/lib/autoland.js +6 -6
  108. package/lib/bench/engines.js +59 -8
  109. package/lib/bench/report.js +1 -1
  110. package/lib/bench/runner.js +204 -5
  111. package/lib/bench/tree-render.js +76 -0
  112. package/lib/card.js +1 -1
  113. package/lib/chat-log-scan.js +1 -1
  114. package/lib/claude-boot-block.js +2 -0
  115. package/lib/deck-compose.js +1 -1
  116. package/lib/deck-history.js +2 -2
  117. package/lib/deck-layout.js +1 -1
  118. package/lib/deck-review.js +5 -5
  119. package/lib/deck-schema.js +1 -1
  120. package/lib/default-verifier.js +1 -1
  121. package/lib/developer-api.js +3 -0
  122. package/lib/engine-ask.js +66 -9
  123. package/lib/engine-registry.js +15 -2
  124. package/lib/file-ops.js +1 -1
  125. package/lib/fleet.js +71 -35
  126. package/lib/html-render.js +1 -1
  127. package/lib/intents.js +172 -0
  128. package/lib/journal.js +1 -1
  129. package/lib/known-commands.js +2 -2
  130. package/lib/land-green.js +364 -0
  131. package/lib/lesson-contradiction.js +2 -2
  132. package/lib/lesson-preflight.js +1 -1
  133. package/lib/manifest.js +5 -5
  134. package/lib/member-alive.js +9 -5
  135. package/lib/memory-view.js +3 -3
  136. package/lib/mission-protected-lane.js +1 -1
  137. package/lib/mission-root.js +2 -2
  138. package/lib/orb-context.js +10 -10
  139. package/lib/pack-capabilities.js +861 -3
  140. package/lib/permission-grants.js +2 -2
  141. package/lib/policy-lessons.js +6 -6
  142. package/lib/pulse.js +8 -8
  143. package/lib/receipt-evidence.js +1 -1
  144. package/lib/reel.js +1 -1
  145. package/lib/review-integrity.js +1 -1
  146. package/lib/runner-command.js +35 -5
  147. package/lib/scorecard.js +4 -4
  148. package/lib/security-scan.js +5 -5
  149. package/lib/self-drive.js +1 -1
  150. package/lib/site-publish.js +372 -0
  151. package/lib/site.js +1 -1
  152. package/lib/slides-deck.js +2 -2
  153. package/lib/state-detection.js +2 -2
  154. package/lib/sync-telemetry.js +2 -2
  155. package/lib/task-db.js +17 -10
  156. package/lib/task-explanation.js +1 -1
  157. package/lib/task-proof.js +2 -2
  158. package/lib/task-receipt.js +3 -3
  159. package/lib/theme.js +12 -12
  160. package/lib/todo-fallback.js +3 -3
  161. package/lib/todo-sections.js +1 -1
  162. package/lib/tree-hash.js +79 -0
  163. package/lib/usage.js +1 -1
  164. package/lib/wish-delegate.js +1 -1
  165. package/lib/workspace-safety.js +4 -4
  166. package/lib/workspace-scaffold.js +3 -3
  167. package/package.json +1 -1
  168. package/scripts/det/concierge-bakeoff.sh +26 -0
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +0 -3
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
@@ -30,7 +30,9 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
30
30
  output(`Usage: ${commandName} search "<query>" [--limit N] [--json]`);
31
31
  output(` ${commandName} search --paid "<query>" [--limit N] [--json]`);
32
32
  output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine] [--save]`);
33
- output(` ${commandName} teach <youtube-url> [--section N] [--save]`);
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,22 +42,27 @@ 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('process = 5 credits cloud knowledge (needs a filled Apply)');
48
- output('digest = one decision page from this week\'s video briefs');
49
- 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');
50
53
  output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
51
54
  output('Falls back to cloud video processing when local captions are unavailable.');
52
55
  output('');
53
56
  output('Options:');
54
57
  output(' --limit <n> Max search results (default: 5)');
55
58
  output(' --paid Bill 5 credits for watch permalinks (search only)');
56
- output(' --save File brief, journal, apply stub; teach also mints a keep/revert experiment');
59
+ output(' --save File brief, journal, apply; rich notes/teach mint a keep/revert experiment');
57
60
  output(' --section <n> Chapter to teach, 1-based (teach only, default: 1)');
58
- output(' --unsave Delete filed brief and apply stub (no paid calls)');
61
+ output(' --recap <text> Unlock the next teach section with the unpaid check');
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)');
65
+ output(' --unsave Delete filed brief, apply stub, and matching notes/teach experiment packs (no paid calls)');
59
66
  output(' --query, -q <text> Focus question for the analysis');
60
67
  output(' --agent <id> Agent id to store knowledge against');
61
68
  output(' --store Save as agent knowledge (requires --agent)');
@@ -74,6 +81,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
74
81
  output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID --save`);
75
82
  output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID"`);
76
83
  output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID" --section 2`);
84
+ output(` ${commandName} teach next`);
77
85
  output(` ${commandName} notes --unsave VIDEO_ID`);
78
86
  output(` ${commandName} unsave VIDEO_ID`);
79
87
  output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID https://youtu.be/OTHER_ID`);
@@ -504,6 +512,17 @@ async function processYoutube(options, deps = {}) {
504
512
  return result.data;
505
513
  }
506
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
+
507
526
  function formatYoutubeResult(data) {
508
527
  const lines = [];
509
528
  const metadata = data?.metadata || {};
@@ -523,7 +542,7 @@ function formatYoutubeResult(data) {
523
542
  const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
524
543
  lines.push(`Credits: ${used} used, ${remaining} remaining`);
525
544
  }
526
- const analysis = data?.video_analysis || data?.analysis || data?.result;
545
+ const analysis = processAnalysisText(data);
527
546
  if (analysis) {
528
547
  lines.push('');
529
548
  lines.push(String(analysis).trim());
@@ -564,22 +583,24 @@ function parseNotesArgs(argv = []) {
564
583
  let help = false;
565
584
  let save = false;
566
585
  let unsave = false;
586
+ let json = false;
567
587
  for (const raw of argv) {
568
588
  const arg = String(raw);
569
589
  if (arg === '--help' || arg === '-h' || arg === 'help') help = true;
570
590
  else if (arg === '--save') save = true;
571
591
  else if (arg === '--unsave') unsave = true;
592
+ else if (arg === '--json') json = true;
572
593
  }
573
594
  for (const raw of argv) {
574
595
  const arg = String(raw);
575
596
  if (arg === '--help' || arg === '-h' || arg === 'help') continue;
576
- if (arg === '--save' || arg === '--unsave') continue;
597
+ if (arg === '--save' || arg === '--unsave' || arg === '--json') continue;
577
598
  if (arg.startsWith('-')) continue;
578
599
  if (looksLikeYoutubeUrl(arg)) urls.push(arg);
579
600
  else if (unsave && videoIdFromArg(arg)) urls.push(arg);
580
601
  else engine = arg;
581
602
  }
582
- return { urls, engine, help, save, unsave };
603
+ return { urls, engine, help, save, unsave, json };
583
604
  }
584
605
 
585
606
  function dateStamp(now) {
@@ -645,16 +666,69 @@ function applySidecarRel(id) {
645
666
  return applyGate.applySidecarRel('youtube', id);
646
667
  }
647
668
 
648
- function ensureNotesApply({ cwd, url, now, output } = {}) {
669
+ function notesExperimentSlug(id) {
670
+ return `notes-${experimentIdToken(id)}`;
671
+ }
672
+
673
+ function notesExperimentRel(id) {
674
+ return `atris/experiments/${notesExperimentSlug(id)}`;
675
+ }
676
+
677
+ function notesLessonFromText(text) {
678
+ const body = String(text || '');
679
+ return {
680
+ numbers: extractTeachNumbers(body),
681
+ mechanisms: extractTeachMechanisms(body),
682
+ };
683
+ }
684
+
685
+ function readNotesText({ url, workDir } = {}) {
686
+ const id = videoIdFromUrl(url);
687
+ if (!id || !workDir) return '';
688
+ const notesPath = path.join(workDir, `yt_${id}.md`);
689
+ try {
690
+ if (!fs.existsSync(notesPath)) return '';
691
+ return fs.readFileSync(notesPath, 'utf8');
692
+ } catch {
693
+ return '';
694
+ }
695
+ }
696
+
697
+ function saveRichNotes(url, deps = {}) {
698
+ const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
699
+ const lesson = notesLessonFromText(readNotesText({ url, workDir }));
700
+ if (isThinTeachLesson(lesson)) {
701
+ return { thin: true, brief: null, packRel: null, lesson };
702
+ }
703
+ const brief = fileNotesBrief(url, deps);
649
704
  const id = videoIdFromUrl(url);
705
+ const packRel = fileTeachExperiment({
706
+ cwd: deps.cwd || process.cwd(),
707
+ url,
708
+ lesson,
709
+ slug: id ? notesExperimentSlug(id) : null,
710
+ applyRel: id ? applySidecarRel(id) : null,
711
+ });
712
+ return { thin: false, brief, packRel, lesson };
713
+ }
714
+
715
+ function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
716
+ const id = videoIdFromUrl(url);
717
+ const pack = packRel || (id ? notesExperimentRel(id) : null);
718
+ const slug = pack ? path.basename(pack) : null;
650
719
  return applyGate.ensureApply({
651
720
  cwd,
652
721
  source: url,
653
722
  rel: id ? applySidecarRel(id) : null,
654
723
  now,
655
724
  output,
656
- incompleteMessage: APPLY_NEXT_MESSAGE,
725
+ incompleteMessage: slug
726
+ ? `next: atris experiments keep ${slug}`
727
+ : APPLY_NEXT_MESSAGE,
657
728
  required: false,
729
+ change: pack ? `apply ${pack}` : undefined,
730
+ receipt: pack ? TEACH_KEEP_RULE : undefined,
731
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
658
732
  });
659
733
  }
660
734
 
@@ -662,6 +736,50 @@ function youtubeBriefRel(id) {
662
736
  return `atris/wiki/briefs/youtube-${id}.md`;
663
737
  }
664
738
 
739
+ function removeUnsaveRel(cwd, rel, removed) {
740
+ const abs = path.join(cwd, rel);
741
+ try {
742
+ if (!fs.existsSync(abs)) return;
743
+ const st = fs.lstatSync(abs);
744
+ if (st.isDirectory()) fs.rmSync(abs, { recursive: true, force: true });
745
+ else fs.unlinkSync(abs);
746
+ removed.push(rel);
747
+ } catch {
748
+ // already gone or unreadable: do not error
749
+ }
750
+ }
751
+
752
+ function listTeachSectionNumbers(cwd, id) {
753
+ const root = path.join(cwd, 'atris', 'experiments');
754
+ const sections = [];
755
+ try {
756
+ if (!fs.existsSync(root)) return sections;
757
+ for (const name of fs.readdirSync(root)) {
758
+ const match = String(name).match(/^teach-.+-s(\d+)$/);
759
+ if (!match) continue;
760
+ const section = Number(match[1]);
761
+ if (name === teachExperimentSlug(id, section)) sections.push(section);
762
+ }
763
+ } catch {
764
+ // missing experiments dir is fine
765
+ }
766
+ return sections.sort((a, b) => a - b);
767
+ }
768
+
769
+ function listTeachSidecarRels(cwd, id) {
770
+ const briefsDir = path.join(cwd, 'atris', 'wiki', 'briefs');
771
+ const prefix = `youtube-${id}-s`;
772
+ try {
773
+ if (!fs.existsSync(briefsDir)) return [];
774
+ return fs.readdirSync(briefsDir)
775
+ .filter((name) => name.startsWith(prefix) && name.endsWith('.md'))
776
+ .sort((a, b) => a.localeCompare(b, 'en'))
777
+ .map((name) => `atris/wiki/briefs/${name}`);
778
+ } catch {
779
+ return [];
780
+ }
781
+ }
782
+
665
783
  function unsaveYoutubeNotes(target, deps = {}) {
666
784
  const output = deps.output || ((line = '') => console.log(line));
667
785
  const cwd = deps.cwd || process.cwd();
@@ -672,18 +790,26 @@ function unsaveYoutubeNotes(target, deps = {}) {
672
790
  }
673
791
  const briefRel = youtubeBriefRel(id);
674
792
  const applyRel = applySidecarRel(id);
675
- const removed = [];
676
- for (const rel of [briefRel, applyRel]) {
677
- const abs = path.join(cwd, rel);
678
- try {
679
- if (fs.existsSync(abs)) {
680
- fs.unlinkSync(abs);
681
- removed.push(rel);
682
- }
683
- } catch {
684
- // already gone or unreadable: do not error
685
- }
793
+ const sections = listTeachSectionNumbers(cwd, id);
794
+ const rels = [];
795
+ const seen = new Set();
796
+ const add = (rel) => {
797
+ if (!rel || seen.has(rel)) return;
798
+ seen.add(rel);
799
+ rels.push(rel);
800
+ };
801
+ add(briefRel);
802
+ add(applyRel);
803
+ for (const section of sections) {
804
+ add(teachBriefRel(id, section));
805
+ add(applySidecarRel(`${id}-s${section}`));
686
806
  }
807
+ for (const rel of listTeachSidecarRels(cwd, id)) add(rel);
808
+ add(notesExperimentRel(id));
809
+ for (const section of sections) add(teachExperimentRel(id, section));
810
+
811
+ const removed = [];
812
+ for (const rel of rels) removeUnsaveRel(cwd, rel, removed);
687
813
  if (!removed.length) {
688
814
  output(`already gone: ${briefRel} and ${applyRel}`);
689
815
  return 0;
@@ -708,6 +834,9 @@ function runYoutubeUnsave(args = [], deps = {}) {
708
834
  const status = unsaveYoutubeNotes(target, deps);
709
835
  if (status !== 0) code = status;
710
836
  }
837
+ if (code === 0 && parsed.json !== true && deps.json !== true) {
838
+ printWatchSearchNext(output);
839
+ }
711
840
  return code;
712
841
  }
713
842
 
@@ -870,6 +999,104 @@ function appendDigestJournal({ cwd, date, relDigest }) {
870
999
  fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
871
1000
  }
872
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
+
873
1100
  function runYoutubeDigest(args = [], deps = {}) {
874
1101
  const output = deps.output || ((line = '') => console.log(line));
875
1102
  let options;
@@ -889,6 +1116,7 @@ function runYoutubeDigest(args = [], deps = {}) {
889
1116
  const briefs = collectVideoBriefs({ cwd, now, days: options.days });
890
1117
  if (!briefs.length) {
891
1118
  output(`no video briefs in the last ${options.days} days`);
1119
+ printWatchSearchNext(output);
892
1120
  return 0;
893
1121
  }
894
1122
 
@@ -916,7 +1144,32 @@ function runYoutubeDigest(args = [], deps = {}) {
916
1144
  fs.writeFileSync(path.join(cwd, relDigest), `${header}\n\n${text}\n`);
917
1145
  appendDigestJournal({ cwd, date, relDigest });
918
1146
  output(`digest filed: ${relDigest} (${briefs.length} briefs)`);
919
- 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;
920
1173
  }
921
1174
 
922
1175
  function watchStatePath(cwd = process.cwd()) {
@@ -1065,6 +1318,22 @@ function markSeen(state, channel, id, timestamp) {
1065
1318
  state.seenByChannel[channel][id] = timestamp;
1066
1319
  }
1067
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
+
1068
1337
  function addWatchChannel(channelInput, deps = {}) {
1069
1338
  const output = deps.output || ((line = '') => console.log(line));
1070
1339
  if (!channelInput) {
@@ -1084,6 +1353,7 @@ function addWatchChannel(channelInput, deps = {}) {
1084
1353
  const state = loadWatchState(statePath);
1085
1354
  if (state.channels.some((row) => row.channel === channel)) {
1086
1355
  output(`already watching ${channel}`);
1356
+ printWatchTickNext(output);
1087
1357
  return 0;
1088
1358
  }
1089
1359
 
@@ -1093,6 +1363,7 @@ function addWatchChannel(channelInput, deps = {}) {
1093
1363
  });
1094
1364
  saveWatchState(statePath, state);
1095
1365
  output(`watching ${channel}`);
1366
+ printWatchTickNext(output);
1096
1367
  return 0;
1097
1368
  }
1098
1369
 
@@ -1101,6 +1372,7 @@ function listWatchChannels(deps = {}) {
1101
1372
  const state = loadWatchState(resolveWatchStatePath(deps));
1102
1373
  if (!state.channels.length) {
1103
1374
  output('no channels watched');
1375
+ printWatchAddNext(output);
1104
1376
  return 0;
1105
1377
  }
1106
1378
 
@@ -1108,6 +1380,7 @@ function listWatchChannels(deps = {}) {
1108
1380
  const count = Object.keys(channelSeenMap(state, row.channel)).length;
1109
1381
  output(`${index + 1}. ${row.channel} (${count} seen)`);
1110
1382
  });
1383
+ printWatchTickNext(output);
1111
1384
  return 0;
1112
1385
  }
1113
1386
 
@@ -1124,6 +1397,11 @@ function removeWatchChannel(rawNumber, deps = {}) {
1124
1397
  const [removed] = state.channels.splice(index - 1, 1);
1125
1398
  saveWatchState(statePath, state);
1126
1399
  output(`removed ${removed.channel}`);
1400
+ if (!state.channels.length) {
1401
+ printWatchAddNext(output);
1402
+ } else {
1403
+ printWatchTickNext(output);
1404
+ }
1127
1405
  return 0;
1128
1406
  }
1129
1407
 
@@ -1141,6 +1419,7 @@ async function tickWatch(deps = {}) {
1141
1419
  const state = loadWatchState(statePath);
1142
1420
  let totalNew = 0;
1143
1421
  let totalBriefed = 0;
1422
+ let firstBriefedUrl = null;
1144
1423
 
1145
1424
  for (const row of state.channels) {
1146
1425
  const videosUrl = channelVideosUrl(row.channel);
@@ -1187,6 +1466,7 @@ async function tickWatch(deps = {}) {
1187
1466
  }
1188
1467
  markSeen(state, row.channel, video.id, timestamp);
1189
1468
  briefed += 1;
1469
+ if (!firstBriefedUrl) firstBriefedUrl = url;
1190
1470
  }
1191
1471
 
1192
1472
  if (!isFresh) {
@@ -1204,6 +1484,40 @@ async function tickWatch(deps = {}) {
1204
1484
 
1205
1485
  output(`total: ${totalNew} new, ${totalBriefed} briefed`);
1206
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);
1207
1521
  return 0;
1208
1522
  }
1209
1523
 
@@ -1339,11 +1653,34 @@ function runOneNotesItem(item, engine, deps = {}) {
1339
1653
  } catch {
1340
1654
  status = 1;
1341
1655
  }
1342
- const brief = status === 0 && deps.save ? fileNotesBrief(item.url, deps) : null;
1656
+ let brief = null;
1657
+ let lesson = null;
1658
+ let ok = status === 0;
1659
+ if (ok && deps.save) {
1660
+ const saved = saveRichNotes(item.url, deps);
1661
+ if (saved.thin) {
1662
+ output(TEACH_THIN_REFUSE);
1663
+ ok = false;
1664
+ } else {
1665
+ brief = saved.brief;
1666
+ lesson = saved.lesson;
1667
+ const ensureApply = deps.ensureApply || ensureNotesApply;
1668
+ try {
1669
+ ensureApply({
1670
+ cwd: deps.cwd || process.cwd(),
1671
+ url: item.url,
1672
+ packRel: saved.packRel,
1673
+ now: deps.now,
1674
+ output,
1675
+ });
1676
+ } catch {
1677
+ // apply filing must never break the batch
1678
+ }
1679
+ }
1680
+ }
1343
1681
  const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
1344
- const ok = status === 0;
1345
1682
  output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
1346
- return { url: item.url, id: item.id, seconds, ok, brief };
1683
+ return { url: item.url, id: item.id, seconds, ok, brief, lesson };
1347
1684
  }
1348
1685
 
1349
1686
  function formatNotesSummary(rows = []) {
@@ -1355,7 +1692,14 @@ function formatNotesSummary(rows = []) {
1355
1692
  return lines.join('\n');
1356
1693
  }
1357
1694
 
1358
- 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 = {}) {
1359
1703
  deps = { ...deps, save: save === true || deps.save === true };
1360
1704
  const output = deps.output || ((line = '') => console.error(line));
1361
1705
  const items = expandNotesTargets(urls || [], deps);
@@ -1368,7 +1712,30 @@ function runYoutubeNotesBatch({ urls, engine, save } = {}, deps = {}) {
1368
1712
  output(formatNotesSummary(rows));
1369
1713
  }
1370
1714
  if (!rows.length) return 2;
1371
- 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;
1372
1739
  }
1373
1740
 
1374
1741
  function runSingleYoutubeNotes(url, engine, deps = {}) {
@@ -1380,15 +1747,39 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
1380
1747
  }
1381
1748
  const status = readRunnerStatus(result);
1382
1749
  if (status !== 0) return status;
1383
- if (!deps.save) return 0;
1384
- fileNotesBrief(url, deps);
1750
+ const output = deps.output || ((line = '') => console.error(line));
1751
+ if (!deps.save) {
1752
+ const json = deps.json === true;
1753
+ printEphemeralNotesLearnerGate(url, { json, workDir: deps.workDir }, output);
1754
+ printYoutubeTeachNext(url, { json }, output);
1755
+ return 0;
1756
+ }
1757
+ const saved = saveRichNotes(url, deps);
1758
+ if (saved.thin) {
1759
+ output(TEACH_THIN_REFUSE);
1760
+ printYoutubeTeachNext(url, { json: deps.json }, output);
1761
+ return 2;
1762
+ }
1385
1763
  const ensureApply = deps.ensureApply || ensureNotesApply;
1386
- return ensureApply({
1387
- cwd: deps.cwd || process.cwd(),
1764
+ const cwd = deps.cwd || process.cwd();
1765
+ const applyCode = ensureApply({
1766
+ cwd,
1388
1767
  url,
1768
+ packRel: saved.packRel,
1389
1769
  now: deps.now,
1390
1770
  output: deps.output,
1391
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;
1392
1783
  }
1393
1784
 
1394
1785
  function runYoutubeNotes(args = [], deps = {}) {
@@ -1406,7 +1797,7 @@ function runYoutubeNotes(args = [], deps = {}) {
1406
1797
  output(YTNOTES_HINT);
1407
1798
  return 2;
1408
1799
  }
1409
- const nextDeps = { ...deps, save: parsed.save };
1800
+ const nextDeps = { ...deps, save: parsed.save, json: parsed.json };
1410
1801
  if (parsed.urls.length === 1 && !isPlaylistUrl(parsed.urls[0])) {
1411
1802
  return runSingleYoutubeNotes(parsed.urls[0], parsed.engine, nextDeps);
1412
1803
  }
@@ -1436,10 +1827,13 @@ function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtub
1436
1827
  output('');
1437
1828
  output('Free local discovery. Uses ytsearch on PATH when present, else the');
1438
1829
  output('bundled scripts/det/ytsearch, else yt-dlp ytsearchN with the same print contract.');
1439
- 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.');
1440
1832
  output('');
1441
1833
  output(`--paid buys watch permalinks from Atris (${PAID_SEARCH_COST_HINT}).`);
1442
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.');
1443
1837
  output('Empty or failed paid search refunds the credits.');
1444
1838
  output('');
1445
1839
  output('Options:');
@@ -1570,6 +1964,17 @@ function resolveSearchCachePath(deps = {}) {
1570
1964
  return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
1571
1965
  }
1572
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
+
1573
1978
  function printSearchRows(rows, options, output) {
1574
1979
  if (options.json) {
1575
1980
  output(JSON.stringify(rows, null, 2));
@@ -1769,12 +2174,29 @@ async function runPaidYoutubeSearch(options, deps = {}) {
1769
2174
  const rendered = formatPaidSearchResults(data);
1770
2175
  if (!videos.length) {
1771
2176
  output(rendered ? `no videos found\n${rendered}` : 'no videos found');
2177
+ printWatchTickNext(output);
1772
2178
  return 2;
1773
2179
  }
1774
2180
  output(rendered);
2181
+ printSearchLearnerGate(videos, options, output);
2182
+ printSearchTeachNext(videos, options, output);
1775
2183
  return 0;
1776
2184
  }
1777
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
+
1778
2200
  function commandOnPath(name, deps = {}) {
1779
2201
  const spawn = deps.spawnSync || spawnSync;
1780
2202
  const result = spawn('sh', ['-c', `command -v ${shellSingleQuote(name)}`], {
@@ -1910,6 +2332,7 @@ async function runYoutubeSearch(args = [], deps = {}) {
1910
2332
  if (cached) {
1911
2333
  const rows = cached.rows.slice(0, options.limit);
1912
2334
  printSearchRows(rows, options, output);
2335
+ printSearchTeachNext(rows, options, output);
1913
2336
  output(LOCAL_SEARCH_CACHE_NOTE);
1914
2337
  return 0;
1915
2338
  }
@@ -1928,17 +2351,26 @@ async function runYoutubeSearch(args = [], deps = {}) {
1928
2351
  const rows = parseSearchStdout(stdout);
1929
2352
  if (!rows.length) {
1930
2353
  output('no videos found');
2354
+ if (!options.json) printWatchTickNext(output);
1931
2355
  return 2;
1932
2356
  }
1933
2357
 
1934
2358
  writeLocalSearchCache(options.query, rows, deps);
1935
2359
  printSearchRows(rows, options, output);
2360
+ printSearchLearnerGate(rows, options, output);
2361
+ printSearchTeachNext(rows, options, output);
1936
2362
  return 0;
1937
2363
  }
1938
2364
 
1939
- const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save]';
2365
+ const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip] | owed | next';
1940
2366
  const TEACH_PAID_REFUSE = 'teach is free local captions. drop --paid.';
1941
2367
  const TEACH_THIN_REFUSE = 'thin: no number or named mechanism. no brief.';
2368
+ const TEACH_OWED_FILE = 'youtube-teach-owed.json';
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';
1942
2374
  const TEACH_APPLY_NEXT_MESSAGE = APPLY_NEXT_MESSAGE;
1943
2375
  const TEACH_KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
1944
2376
  const MECHANISM_STOP = new Set([
@@ -1965,11 +2397,18 @@ function parseTeachArgs(argv = []) {
1965
2397
  const options = {
1966
2398
  help: false,
1967
2399
  save: false,
2400
+ json: false,
2401
+ skip: false,
2402
+ owed: false,
2403
+ resume: false,
2404
+ next: false,
2405
+ recap: null,
1968
2406
  url: null,
1969
2407
  section: 1,
1970
2408
  };
2409
+ let sectionSet = false;
1971
2410
 
1972
- if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
2411
+ if (args.length > 0 && ['help', '--help', '-h'].includes(args[0])) {
1973
2412
  options.help = true;
1974
2413
  return options;
1975
2414
  }
@@ -1980,8 +2419,25 @@ function parseTeachArgs(argv = []) {
1980
2419
  options.help = true;
1981
2420
  } else if (arg === '--save') {
1982
2421
  options.save = true;
2422
+ } else if (arg === '--json') {
2423
+ options.json = true;
2424
+ } else if (arg === '--skip') {
2425
+ options.skip = true;
2426
+ } else if (arg === '--owed') {
2427
+ options.owed = true;
1983
2428
  } else if (arg === '--paid') {
1984
2429
  throw new Error(TEACH_PAID_REFUSE);
2430
+ } else if (arg === '--recap') {
2431
+ const raw = args[i + 1];
2432
+ if (raw == null || String(raw).startsWith('--')) {
2433
+ throw new Error(TEACH_RECAP_MISSING);
2434
+ }
2435
+ options.recap = String(raw);
2436
+ i += 1;
2437
+ } else if (arg.startsWith('--recap=')) {
2438
+ const value = arg.slice('--recap='.length);
2439
+ if (!value) throw new Error(TEACH_RECAP_MISSING);
2440
+ options.recap = value;
1985
2441
  } else if (arg === '--section') {
1986
2442
  const raw = args[i + 1];
1987
2443
  const value = Number.parseInt(raw, 10);
@@ -1989,6 +2445,7 @@ function parseTeachArgs(argv = []) {
1989
2445
  throw new Error('--section must be a positive integer');
1990
2446
  }
1991
2447
  options.section = value;
2448
+ sectionSet = true;
1992
2449
  i += 1;
1993
2450
  } else if (arg.startsWith('--section=')) {
1994
2451
  const value = Number.parseInt(arg.slice('--section='.length), 10);
@@ -1996,8 +2453,25 @@ function parseTeachArgs(argv = []) {
1996
2453
  throw new Error('--section must be a positive integer');
1997
2454
  }
1998
2455
  options.section = value;
2456
+ sectionSet = true;
1999
2457
  } else if (arg.startsWith('-')) {
2000
2458
  throw new Error(`Unknown option: ${arg}`);
2459
+ } else if (arg === 'recap' && options.recap == null && !options.url) {
2460
+ const parts = [];
2461
+ i += 1;
2462
+ while (i < args.length && !String(args[i]).startsWith('--')) {
2463
+ parts.push(String(args[i]));
2464
+ i += 1;
2465
+ }
2466
+ i -= 1;
2467
+ options.recap = parts.join(' ').trim();
2468
+ if (!options.recap) throw new Error(TEACH_RECAP_MISSING);
2469
+ } else if (arg === 'skip' && !options.url) {
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;
2001
2475
  } else if (!options.url && looksLikeYoutubeUrl(arg)) {
2002
2476
  options.url = arg;
2003
2477
  } else {
@@ -2006,7 +2480,14 @@ function parseTeachArgs(argv = []) {
2006
2480
  }
2007
2481
 
2008
2482
  if (options.help) return options;
2009
- if (!options.url) throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
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
+ }
2489
+ throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
2490
+ }
2010
2491
  return options;
2011
2492
  }
2012
2493
 
@@ -2124,7 +2605,13 @@ function extractTeachMechanisms(text) {
2124
2605
  return found.slice(0, 8);
2125
2606
  }
2126
2607
 
2127
- 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) {
2128
2615
  if (mechanisms[0]) {
2129
2616
  const name = mechanisms[0];
2130
2617
  const named = new RegExp(`\\b(?:${MECHANISM_HEADS})\\b`, 'i').test(name);
@@ -2132,7 +2619,63 @@ function oneTeachCheck(mechanisms, numbers, title) {
2132
2619
  return `what is ${name}?`;
2133
2620
  }
2134
2621
  if (numbers[0]) return `what does ${numbers[0]} measure in this chapter?`;
2135
- 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;
2136
2679
  }
2137
2680
 
2138
2681
  function quoteYoutubeUrl(url) {
@@ -2163,10 +2706,7 @@ function teachLessonFromCues({ url, section, chapters, chapter, cues, title } =
2163
2706
  lines.push(oneTeachCheck(mechanisms, numbers, heading));
2164
2707
  if (section < total) {
2165
2708
  lines.push('');
2166
- lines.push(`next: atris youtube teach ${quoteYoutubeUrl(url)} --section ${section + 1}`);
2167
- } else {
2168
- lines.push('');
2169
- lines.push('next: last section');
2709
+ lines.push(TEACH_RESUME_NEXT);
2170
2710
  }
2171
2711
  return { text: lines.join('\n'), numbers, mechanisms };
2172
2712
  }
@@ -2185,14 +2725,17 @@ function teachBriefRel(id, section) {
2185
2725
  return `atris/wiki/briefs/youtube-${id}-s${section}.md`;
2186
2726
  }
2187
2727
 
2188
- function teachExperimentSlug(id, section) {
2189
- const safe = String(id || 'video')
2728
+ function experimentIdToken(id) {
2729
+ return String(id || 'video')
2190
2730
  .toLowerCase()
2191
2731
  .replace(/_/g, '-')
2192
2732
  .replace(/[^a-z0-9-]+/g, '')
2193
2733
  .replace(/-+/g, '-')
2194
2734
  .replace(/^-+|-+$/g, '') || 'video';
2195
- return `teach-${safe}-s${Number(section) || 1}`;
2735
+ }
2736
+
2737
+ function teachExperimentSlug(id, section) {
2738
+ return `teach-${experimentIdToken(id)}-s${Number(section) || 1}`;
2196
2739
  }
2197
2740
 
2198
2741
  function teachExperimentRel(id, section) {
@@ -2207,22 +2750,256 @@ function teachCheckNeedles(lesson = {}) {
2207
2750
  return [];
2208
2751
  }
2209
2752
 
2753
+ function teachRecapTokens(lesson = {}) {
2754
+ const tokens = [];
2755
+ const seen = new Set();
2756
+ for (const value of [...(lesson.mechanisms || []), ...(lesson.numbers || [])]) {
2757
+ const token = String(value || '').trim().toLowerCase();
2758
+ if (!token || seen.has(token)) continue;
2759
+ seen.add(token);
2760
+ tokens.push(token);
2761
+ }
2762
+ return tokens;
2763
+ }
2764
+
2765
+ function recapHitsTokens(text, tokens) {
2766
+ const hay = String(text || '').toLowerCase();
2767
+ return (tokens || []).some((token) => {
2768
+ const needle = String(token || '').trim().toLowerCase();
2769
+ return needle.length > 0 && hay.includes(needle);
2770
+ });
2771
+ }
2772
+
2773
+ function teachOwedPath(deps = {}) {
2774
+ if (deps.teachOwedPath) return deps.teachOwedPath;
2775
+ const cwd = deps.cwd || process.cwd();
2776
+ return path.join(cwd, '.atris', TEACH_OWED_FILE);
2777
+ }
2778
+
2779
+ function readTeachOwedStore(deps = {}) {
2780
+ try {
2781
+ const parsed = JSON.parse(fs.readFileSync(teachOwedPath(deps), 'utf8'));
2782
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
2783
+ return parsed;
2784
+ } catch {
2785
+ return {};
2786
+ }
2787
+ }
2788
+
2789
+ function writeTeachOwedStore(deps, store) {
2790
+ try {
2791
+ const filePath = teachOwedPath(deps);
2792
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
2793
+ fs.writeFileSync(filePath, `${JSON.stringify(store || {})}\n`);
2794
+ } catch {
2795
+ // owed recap is ephemeral; a failed write must not break teach
2796
+ }
2797
+ }
2798
+
2799
+ function rememberTeachOwed(deps, { url, section, lesson, total } = {}) {
2800
+ const id = videoIdFromUrl(url);
2801
+ if (!id) return;
2802
+ const store = readTeachOwedStore(deps);
2803
+ const row = {
2804
+ section: Number(section) || 1,
2805
+ check: teachCheckLine(lesson),
2806
+ tokens: teachRecapTokens(lesson),
2807
+ url: url || undefined,
2808
+ };
2809
+ const totalChapters = Number(total);
2810
+ if (Number.isInteger(totalChapters) && totalChapters > 0) row.total = totalChapters;
2811
+ store[id] = row;
2812
+ writeTeachOwedStore(deps, store);
2813
+ }
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
+
2865
+ function previousTeachUnlocked(owed, section) {
2866
+ if (!owed) return true;
2867
+ return Number(owed.section) >= Number(section);
2868
+ }
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
+
2918
+ function applyTeachRecap(parsed, deps, output) {
2919
+ const store = readTeachOwedStore(deps);
2920
+ let id = parsed.url ? videoIdFromUrl(parsed.url) : null;
2921
+ let entry = id ? store[id] : null;
2922
+ if (!entry && !id) {
2923
+ const ids = Object.keys(store);
2924
+ const unpaidIds = ids.filter((key) => store[key] && String(store[key].check || '').trim());
2925
+ if (parsed.recap) {
2926
+ id = unpaidIds.find((key) => recapHitsTokens(parsed.recap, store[key] && store[key].tokens));
2927
+ }
2928
+ if (!id && unpaidIds.length === 1) id = unpaidIds[0];
2929
+ if (!id && ids.length === 1) id = ids[0];
2930
+ entry = id ? store[id] : null;
2931
+ }
2932
+ if (!entry) {
2933
+ if (parsed.skip) return 0;
2934
+ if (!parsed.json) output('recap the previous section first');
2935
+ return 2;
2936
+ }
2937
+ if (!String(entry.check || '').trim()) {
2938
+ printTeachUnlockNext(parsed, entry, output);
2939
+ return 0;
2940
+ }
2941
+ if (parsed.skip) {
2942
+ printTeachUnlockNext(parsed, entry, output);
2943
+ writeTeachContinue(store, id, entry);
2944
+ writeTeachOwedStore(deps, store);
2945
+ return 0;
2946
+ }
2947
+ if (!recapHitsTokens(parsed.recap, entry.tokens)) {
2948
+ if (!parsed.json) output(entry.check);
2949
+ return 2;
2950
+ }
2951
+ printTeachUnlockNext(parsed, entry, output);
2952
+ writeTeachContinue(store, id, entry);
2953
+ writeTeachOwedStore(deps, store);
2954
+ return 0;
2955
+ }
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
+
2210
2985
  function teachCheckLine(lesson = {}) {
2211
2986
  return oneTeachCheck(lesson.mechanisms || [], lesson.numbers || [], '');
2212
2987
  }
2213
2988
 
2214
- function fileTeachExperiment({ cwd, url, section, lesson } = {}) {
2989
+ function fileTeachExperiment({ cwd, url, section, lesson, slug, applyRel } = {}) {
2215
2990
  try {
2216
- const id = videoIdFromUrl(url);
2217
- if (!id || !cwd) return null;
2218
- const slug = teachExperimentSlug(id, section);
2219
- const rel = `atris/experiments/${slug}`;
2991
+ const id = url ? videoIdFromUrl(url) : null;
2992
+ if (!cwd) return null;
2993
+ const packSlug = slug || (id ? teachExperimentSlug(id, section) : null);
2994
+ if (!packSlug) return null;
2995
+ const rel = `atris/experiments/${packSlug}`;
2220
2996
  const dir = path.join(cwd, rel);
2221
2997
  fs.mkdirSync(dir, { recursive: true });
2222
2998
 
2223
2999
  const check = teachCheckLine(lesson);
2224
3000
  const needles = teachCheckNeedles(lesson);
2225
- const applyRel = applySidecarRel(`${id}-s${section}`);
3001
+ const sidecarRel = applyRel || (id ? applySidecarRel(`${id}-s${section}`) : null);
3002
+ if (!sidecarRel) return null;
2226
3003
  const program = [
2227
3004
  '# Program',
2228
3005
  '',
@@ -2243,7 +3020,7 @@ function fileTeachExperiment({ cwd, url, section, lesson } = {}) {
2243
3020
  'EXPERIMENT_DIR = Path(__file__).resolve().parent',
2244
3021
  `CHECK = ${JSON.stringify(check)}`,
2245
3022
  `NEEDLES = ${JSON.stringify(needles)}`,
2246
- `DEFAULT_TARGET = ${JSON.stringify(applyRel)}`,
3023
+ `DEFAULT_TARGET = ${JSON.stringify(sidecarRel)}`,
2247
3024
  '',
2248
3025
  '',
2249
3026
  'def repo_root() -> Path:',
@@ -2493,14 +3270,15 @@ function fileTeachBrief({ cwd, url, section, lesson, now } = {}) {
2493
3270
  function ensureTeachApply({ cwd, url, section, packRel, now, output } = {}) {
2494
3271
  const id = videoIdFromUrl(url);
2495
3272
  const pack = packRel || (id ? teachExperimentRel(id, section) : null);
3273
+ const slug = pack ? path.basename(pack) : null;
2496
3274
  return applyGate.ensureApply({
2497
3275
  cwd,
2498
3276
  source: url,
2499
3277
  rel: id ? applySidecarRel(`${id}-s${section}`) : null,
2500
3278
  now,
2501
3279
  output,
2502
- incompleteMessage: pack
2503
- ? `next: apply ${pack}. keep only if measure.py moves 0→1`
3280
+ incompleteMessage: slug
3281
+ ? `next: atris experiments keep ${slug}`
2504
3282
  : TEACH_APPLY_NEXT_MESSAGE,
2505
3283
  required: false,
2506
3284
  change: pack ? `apply ${pack}` : undefined,
@@ -2559,6 +3337,37 @@ async function runYoutubeTeach(args = [], deps = {}) {
2559
3337
  return 0;
2560
3338
  }
2561
3339
 
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
+ }
3347
+ if (parsed.recap != null || parsed.skip) {
3348
+ const recapCode = applyTeachRecap(parsed, owedDeps, output);
3349
+ if (recapCode !== 0) return recapCode;
3350
+ if (!parsed.url || parsed.section <= 1) return 0;
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
+ }
3357
+
3358
+ if (!parsed.url) {
3359
+ output('Missing YouTube URL. Run "atris youtube teach --help".');
3360
+ return 2;
3361
+ }
3362
+
3363
+ if (parsed.section > 1) {
3364
+ const owed = readTeachOwedStore(owedDeps)[videoIdFromUrl(parsed.url) || ''];
3365
+ if (!previousTeachUnlocked(owed, parsed.section)) {
3366
+ if (!parsed.json && owed && owed.check) output(owed.check);
3367
+ return 2;
3368
+ }
3369
+ }
3370
+
2562
3371
  const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, deps);
2563
3372
  if (!source || !Array.isArray(source.cues) || !source.cues.length) {
2564
3373
  output('no english captions for this url. teach stays local and will not call process.');
@@ -2582,35 +3391,59 @@ async function runYoutubeTeach(args = [], deps = {}) {
2582
3391
  title: source.title,
2583
3392
  });
2584
3393
  output(lesson.text);
3394
+ rememberTeachOwed(owedDeps, {
3395
+ url: parsed.url,
3396
+ section: parsed.section,
3397
+ lesson,
3398
+ total: chapters.length,
3399
+ });
2585
3400
 
2586
- if (!parsed.save) return 0;
3401
+ if (!parsed.save) {
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);
3405
+ return 0;
3406
+ }
2587
3407
  if (isThinTeachLesson(lesson)) {
2588
3408
  output(TEACH_THIN_REFUSE);
3409
+ printTeachWatchTickNext(parsed, lesson, chapters.length, output);
2589
3410
  return 2;
2590
3411
  }
2591
3412
 
3413
+ const cwd = deps.cwd || process.cwd();
2592
3414
  fileTeachBrief({
2593
- cwd: deps.cwd || process.cwd(),
3415
+ cwd,
2594
3416
  url: parsed.url,
2595
3417
  section: parsed.section,
2596
3418
  lesson: lesson.text,
2597
3419
  now: deps.now,
2598
3420
  });
2599
3421
  const packRel = fileTeachExperiment({
2600
- cwd: deps.cwd || process.cwd(),
3422
+ cwd,
2601
3423
  url: parsed.url,
2602
3424
  section: parsed.section,
2603
3425
  lesson,
2604
3426
  });
2605
3427
  const ensureApply = deps.ensureApply || ensureTeachApply;
2606
- return ensureApply({
2607
- cwd: deps.cwd || process.cwd(),
3428
+ const applyCode = ensureApply({
3429
+ cwd,
2608
3430
  url: parsed.url,
2609
3431
  section: parsed.section,
2610
3432
  packRel,
2611
3433
  now: deps.now,
2612
3434
  output,
2613
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;
2614
3447
  }
2615
3448
 
2616
3449
  async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
@@ -2655,7 +3488,12 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
2655
3488
  let status = 0;
2656
3489
  try {
2657
3490
  const data = await processYoutube(options, deps);
2658
- 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
+ }
2659
3497
  } catch (err) {
2660
3498
  if (!err.applyRequired) output(err.message);
2661
3499
  status = Number.isInteger(err.exitCode) ? err.exitCode : 1;
@@ -2703,8 +3541,21 @@ module.exports = {
2703
3541
  extractTeachNumbers,
2704
3542
  extractTeachMechanisms,
2705
3543
  extractTeachSource,
3544
+ oneTeachCheck,
3545
+ learnerCheckFromLesson,
3546
+ scoreLearnerNeedles,
3547
+ LEARNER_CHECK_FILL,
3548
+ LEARNER_SCORE_ZERO,
3549
+ printLearnerCheckGate,
3550
+ proveSavedLearnerBaseline,
2706
3551
  isThinTeachLesson,
2707
3552
  TEACH_THIN_REFUSE,
3553
+ TEACH_RESUME_NEXT,
3554
+ TEACH_WATCH_TICK_NEXT,
2708
3555
  teachExperimentSlug,
3556
+ notesExperimentSlug,
3557
+ digestExperimentSlug,
3558
+ watchExperimentSlug,
3559
+ fileTeachExperiment,
2709
3560
  youtubeCommand,
2710
3561
  };