atris 3.58.5 → 3.58.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/learn.js CHANGED
@@ -12,6 +12,17 @@ const {
12
12
  getStats,
13
13
  exportMarkdown,
14
14
  } = require('../lib/learnings');
15
+ const applyGate = require('../lib/apply-gate');
16
+ const {
17
+ fileTeachExperiment,
18
+ extractTeachNumbers,
19
+ extractTeachMechanisms,
20
+ isThinTeachLesson,
21
+ printLearnerCheckGate,
22
+ proveSavedLearnerBaseline,
23
+ } = require('./youtube');
24
+
25
+ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
15
26
 
16
27
  function showRecent(limit = 20) {
17
28
  const learnings = loadLearnings()
@@ -149,7 +160,52 @@ function showPrune() {
149
160
  console.log('');
150
161
  }
151
162
 
152
- function interactiveAdd() {
163
+ function commitAddedLearning({ type, key, insight, confidence, source, files } = {}, deps = {}) {
164
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
165
+ const cwd = deps.cwd || process.cwd();
166
+ const entry = addLearning({ type, key, insight, confidence, source, files });
167
+ print('');
168
+ print(` ✓ Saved: [${entry.confidence}/10] ${entry.type}/${entry.key}`);
169
+ print(` "${entry.insight}"`);
170
+ print('');
171
+ const baseline = mintRichLearn({
172
+ cwd,
173
+ key: entry.key,
174
+ insight: entry.insight,
175
+ now: deps.now,
176
+ output: print,
177
+ });
178
+ return { entry, baseline };
179
+ }
180
+
181
+ function reviewLearningType(insight) {
182
+ return /^(don't|never|avoid|watch out|careful)/i.test(String(insight || '')) ? 'pitfall' : 'pattern';
183
+ }
184
+
185
+ function reviewLearningKey(insight) {
186
+ return String(insight || '').toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
187
+ }
188
+
189
+ function commitReviewLearning(insight, deps = {}) {
190
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
191
+ const cwd = deps.cwd || process.cwd();
192
+ const text = String(insight || '').trim();
193
+ if (!text) return { entry: null, baseline: 0 };
194
+ const type = reviewLearningType(text);
195
+ const key = reviewLearningKey(text);
196
+ const entry = addLearning({ type, key, insight: text, confidence: 7, source: 'review', files: [] });
197
+ print(`✓ Saved to learnings: [7/10] ${entry.type}/${entry.key}`);
198
+ const baseline = mintRichLearn({
199
+ cwd,
200
+ key: entry.key,
201
+ insight: entry.insight,
202
+ now: deps.now,
203
+ output: print,
204
+ });
205
+ return { entry, baseline };
206
+ }
207
+
208
+ function interactiveAdd(deps = {}) {
153
209
  const rl = readline.createInterface({
154
210
  input: process.stdin,
155
211
  output: process.stdout,
@@ -214,11 +270,11 @@ function interactiveAdd() {
214
270
  return;
215
271
  }
216
272
 
217
- const entry = addLearning({ type, key, insight, confidence, source, files });
218
- console.log('');
219
- console.log(` ✓ Saved: [${entry.confidence}/10] ${entry.type}/${entry.key}`);
220
- console.log(` "${entry.insight}"`);
221
- console.log('');
273
+ commitAddedLearning({ type, key, insight, confidence, source, files }, {
274
+ cwd: deps.cwd || process.cwd(),
275
+ now: deps.now,
276
+ output: deps.output,
277
+ });
222
278
  rl.close();
223
279
  } catch (err) {
224
280
  console.log(` ✗ Error: ${err.message}`);
@@ -239,24 +295,104 @@ function printLearnLogSchema(stream = console.error) {
239
295
  for (const line of learnLogSchemaLines()) stream(` ${line}`);
240
296
  }
241
297
 
298
+ function learnExperimentSlug(key) {
299
+ return `learn-${applyGate.applySlug(key)}`;
300
+ }
301
+
302
+ function learnExperimentRel(key) {
303
+ return `atris/experiments/${learnExperimentSlug(key)}`;
304
+ }
305
+
306
+ function learnApplyRel(key) {
307
+ return applyGate.applySidecarRel('learn', applyGate.applySlug(key));
308
+ }
309
+
310
+ function learnLessonFromText(text) {
311
+ const body = String(text || '');
312
+ return {
313
+ numbers: extractTeachNumbers(body),
314
+ mechanisms: extractTeachMechanisms(body),
315
+ };
316
+ }
317
+
318
+ function saveRichLearn({ cwd, key, insight } = {}) {
319
+ const lesson = learnLessonFromText(insight);
320
+ if (isThinTeachLesson(lesson)) {
321
+ return { thin: true, packRel: null, lesson };
322
+ }
323
+ if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
324
+ const packRel = fileTeachExperiment({
325
+ cwd,
326
+ lesson,
327
+ slug: key ? learnExperimentSlug(key) : null,
328
+ applyRel: key ? learnApplyRel(key) : null,
329
+ });
330
+ return { thin: false, packRel, lesson };
331
+ }
332
+
333
+ function ensureLearnApply({ cwd, key, packRel, now, output } = {}) {
334
+ const pack = packRel || (key ? learnExperimentRel(key) : null);
335
+ const slug = pack ? path.basename(pack) : null;
336
+ return applyGate.ensureApply({
337
+ cwd,
338
+ source: key ? `learn:${key}` : 'learn',
339
+ rel: key ? learnApplyRel(key) : null,
340
+ now,
341
+ output,
342
+ incompleteMessage: slug
343
+ ? `next: atris experiments keep ${slug}`
344
+ : applyGate.ephemeralApplyMessage('learning'),
345
+ required: false,
346
+ change: pack ? `apply ${pack}` : undefined,
347
+ receipt: pack ? KEEP_RULE : undefined,
348
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${KEEP_RULE}` : undefined,
349
+ });
350
+ }
351
+
352
+ function mintRichLearn({ cwd, key, insight, now, output } = {}) {
353
+ const print = typeof output === 'function' ? output : (line = '') => console.log(line);
354
+ const saved = saveRichLearn({ cwd, key, insight });
355
+ if (saved.thin) {
356
+ printLearnerCheckGate(print, saved.lesson, { includeCheck: true });
357
+ return 0;
358
+ }
359
+ ensureLearnApply({
360
+ cwd,
361
+ key,
362
+ packRel: saved.packRel,
363
+ now,
364
+ output: print,
365
+ });
366
+ return proveSavedLearnerBaseline({
367
+ cwd,
368
+ applyRel: key ? learnApplyRel(key) : null,
369
+ lesson: saved.lesson,
370
+ output: print,
371
+ });
372
+ }
373
+
242
374
  /**
243
375
  * Non-interactive log: `atris learn log '{"type":"pattern","key":"...","insight":"...","confidence":8,"source":"observed"}'`
244
376
  * For agents and scripts, no prompts, no quality gate.
245
377
  * Aliases: title→key, detail→insight.
246
378
  */
247
- function logDirect(jsonStr) {
379
+ function logDirect(jsonStr, deps = {}) {
380
+ const error = typeof deps.error === 'function' ? deps.error : (line = '') => console.error(line);
381
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
382
+ const exit = typeof deps.exit === 'function' ? deps.exit : (code) => process.exit(code);
383
+ const cwd = deps.cwd || process.cwd();
248
384
  if (!jsonStr) {
249
- console.error(' ✗ Usage: atris learn log \'<json>\'');
250
- printLearnLogSchema();
251
- process.exit(1);
385
+ error(' ✗ Usage: atris learn log \'<json>\'');
386
+ printLearnLogSchema(error);
387
+ return exit(1);
252
388
  }
253
389
  let data;
254
390
  try {
255
391
  data = JSON.parse(jsonStr);
256
392
  } catch (err) {
257
- console.error(` ✗ Invalid JSON: ${err.message}`);
258
- printLearnLogSchema();
259
- process.exit(1);
393
+ error(` ✗ Invalid JSON: ${err.message}`);
394
+ printLearnLogSchema(error);
395
+ return exit(1);
260
396
  }
261
397
  try {
262
398
  const entry = addLearning({
@@ -267,11 +403,20 @@ function logDirect(jsonStr) {
267
403
  source: data.source || 'observed',
268
404
  files: data.files || [],
269
405
  });
270
- console.log(` ✓ [${entry.confidence}/10] ${entry.type}/${entry.key}`);
406
+ print(` ✓ [${entry.confidence}/10] ${entry.type}/${entry.key}`);
407
+ const baseline = mintRichLearn({
408
+ cwd,
409
+ key: entry.key,
410
+ insight: entry.insight,
411
+ now: deps.now,
412
+ output: print,
413
+ });
414
+ if (baseline !== 0) return exit(baseline);
415
+ return 0;
271
416
  } catch (err) {
272
- console.error(` ✗ ${err.message}`);
273
- printLearnLogSchema();
274
- process.exit(1);
417
+ error(` ✗ ${err.message}`);
418
+ printLearnLogSchema(error);
419
+ return exit(1);
275
420
  }
276
421
  }
277
422
 
@@ -279,12 +424,14 @@ function logDirect(jsonStr) {
279
424
  * Harvest learnings from journal Notes sections.
280
425
  * Scans recent journals for lines that look like insights.
281
426
  */
282
- function harvestFromJournals() {
283
- const atrisDir = path.join(process.cwd(), 'atris');
427
+ function harvestFromJournals(deps = {}) {
428
+ const print = typeof deps.output === 'function' ? deps.output : (line = '') => console.log(line);
429
+ const cwd = deps.cwd || process.cwd();
430
+ const atrisDir = path.join(cwd, 'atris');
284
431
  const logsDir = path.join(atrisDir, 'logs');
285
432
 
286
433
  if (!fs.existsSync(logsDir)) {
287
- console.log(' No journals found.');
434
+ print(' No journals found.');
288
435
  return;
289
436
  }
290
437
 
@@ -318,10 +465,10 @@ function harvestFromJournals() {
318
465
  }
319
466
 
320
467
  if (candidates.length === 0) {
321
- console.log('');
322
- console.log(' No harvestable notes found in recent journals.');
323
- console.log(' Add notes during "atris review" or write to ## Notes in your journal.');
324
- console.log('');
468
+ print('');
469
+ print(' No harvestable notes found in recent journals.');
470
+ print(' Add notes during "atris review" or write to ## Notes in your journal.');
471
+ print('');
325
472
  return;
326
473
  }
327
474
 
@@ -331,31 +478,38 @@ function harvestFromJournals() {
331
478
  const fresh = candidates.filter(c => !existingInsights.has(c.insight.toLowerCase()));
332
479
 
333
480
  if (fresh.length === 0) {
334
- console.log('');
335
- console.log(` Scanned ${candidates.length} journal notes, all already captured.`);
336
- console.log('');
481
+ print('');
482
+ print(` Scanned ${candidates.length} journal notes, all already captured.`);
483
+ print('');
337
484
  return;
338
485
  }
339
486
 
340
- console.log('');
341
- console.log(` Found ${fresh.length} new ${fresh.length === 1 ? 'note' : 'notes'} to harvest:`);
342
- console.log('');
487
+ print('');
488
+ print(` Found ${fresh.length} new ${fresh.length === 1 ? 'note' : 'notes'} to harvest:`);
489
+ print('');
343
490
  for (let i = 0; i < fresh.length; i++) {
344
491
  const c = fresh[i];
345
492
  const isPitfall = /^(don't|never|avoid|watch out|careful)/i.test(c.insight);
346
493
  const type = isPitfall ? 'pitfall' : 'pattern';
347
494
  const key = c.insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
348
- console.log(` ${i + 1}. [${type}] ${c.insight}`);
349
- console.log(` from: ${c.source}`);
495
+ print(` ${i + 1}. [${type}] ${c.insight}`);
496
+ print(` from: ${c.source}`);
350
497
 
351
498
  try {
352
- addLearning({ type, key, insight: c.insight, confidence: 6, source: 'review', files: [] });
353
- console.log(` ✓ saved [6/10]`);
499
+ const entry = addLearning({ type, key, insight: c.insight, confidence: 6, source: 'review', files: [] });
500
+ print(` ✓ saved [6/10]`);
501
+ mintRichLearn({
502
+ cwd,
503
+ key: entry.key,
504
+ insight: entry.insight,
505
+ now: deps.now,
506
+ output: print,
507
+ });
354
508
  } catch (err) {
355
- console.log(` ✗ ${err.message}`);
509
+ print(` ✗ ${err.message}`);
356
510
  }
357
511
  }
358
- console.log('');
512
+ print('');
359
513
  }
360
514
 
361
515
  /**
@@ -375,10 +529,10 @@ function showLearnHelp() {
375
529
  console.log('');
376
530
  console.log(' Commands:');
377
531
  console.log(' (none) Show recent learnings');
378
- console.log(' add Add a learning interactively');
379
- console.log(' log <json> Add programmatically (for agents)');
532
+ console.log(' add Add a learning interactively. A rich insight mints one apply plus a failing measure.py. A thin insight prints check: fill this.');
533
+ console.log(' log <json> Add programmatically (for agents). A rich insight (number or named mechanism) mints one apply plus a failing measure.py. A thin insight prints check: fill this.');
380
534
  console.log(' search <q> Search learnings by keyword');
381
- console.log(' harvest Extract learnings from journal Notes');
535
+ console.log(' harvest Extract learnings from journal Notes. A rich insight mints one apply plus a failing measure.py. A thin insight prints check: fill this.');
382
536
  console.log(' prune Check for stale/contradictory entries');
383
537
  console.log(' stats Show learning statistics');
384
538
  console.log(' export Export as markdown');
@@ -437,5 +591,17 @@ function learnAtris(subcommand, ...args) {
437
591
  }
438
592
 
439
593
  learnAtris.getLearningCount = getLearningCount;
594
+ learnAtris.learnExperimentSlug = learnExperimentSlug;
595
+ learnAtris.learnExperimentRel = learnExperimentRel;
596
+ learnAtris.learnApplyRel = learnApplyRel;
597
+ learnAtris.learnLessonFromText = learnLessonFromText;
598
+ learnAtris.saveRichLearn = saveRichLearn;
599
+ learnAtris.ensureLearnApply = ensureLearnApply;
600
+ learnAtris.mintRichLearn = mintRichLearn;
601
+ learnAtris.commitAddedLearning = commitAddedLearning;
602
+ learnAtris.commitReviewLearning = commitReviewLearning;
603
+ learnAtris.reviewLearningKey = reviewLearningKey;
604
+ learnAtris.harvestFromJournals = harvestFromJournals;
605
+ learnAtris.logDirect = logDirect;
440
606
 
441
607
  module.exports = learnAtris;
@@ -19,6 +19,7 @@ const { startFirstTalk } = require('../lib/context-gatherer');
19
19
  const { isNonInteractive } = require('../lib/noninteractive');
20
20
  const { loadContext } = require('../lib/state-detection');
21
21
  const { buildToolResultBody } = require('../lib/tool-result-encode');
22
+ const { commitReviewLearning } = require('./learn');
22
23
 
23
24
  function wrapWorkflowText(text, width = 76) {
24
25
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
@@ -1786,15 +1787,10 @@ async function reviewAtris() {
1786
1787
  console.log(`✓ Logged to journal: ${learning}`);
1787
1788
  }
1788
1789
 
1789
- // Also log to structured learnings (if learnings module exists)
1790
1790
  try {
1791
- const { addLearning } = require('../lib/learnings');
1792
- const insight = answer.trim();
1793
- // Auto-classify: starts with "don't" or "never" or "avoid" → pitfall, else pattern
1794
- const type = /^(don't|never|avoid|watch out|careful)/i.test(insight) ? 'pitfall' : 'pattern';
1795
- const key = insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
1796
- addLearning({ type, key, insight, confidence: 7, source: 'review', files: [] });
1797
- console.log(`✓ Saved to learnings: [7/10] ${type}/${key}`);
1791
+ commitReviewLearning(answer.trim(), {
1792
+ cwd: process.cwd(),
1793
+ });
1798
1794
  } catch {
1799
1795
  // learnings module not available, so skip silently
1800
1796
  }
@@ -32,7 +32,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
32
32
  output('Prints to stdout. Rich ephemeral prints one apply next-step, then hands off to atris youtube search (no files).');
33
33
  output('--save files a brief only when the result is rich.');
34
34
  output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
35
- output('Empty or failed search refunds the credits.');
35
+ output('Empty or failed search prints credits refunded only when the server marks a refund.');
36
36
  output('');
37
37
  output('Options:');
38
38
  output(' --limit <n> Max results hint (search only)');
@@ -327,13 +327,6 @@ function xSearchCredits(data) {
327
327
  return { used, remaining, refunded };
328
328
  }
329
329
 
330
- function creditsWereRefunded(credits) {
331
- if (!credits) return false;
332
- if (credits.used === 0) return true;
333
- if (credits.refunded === true) return true;
334
- return typeof credits.refunded === 'number' && credits.refunded > 0;
335
- }
336
-
337
330
  function creditsRefundedExplicitly(credits) {
338
331
  if (!credits) return false;
339
332
  if (credits.refunded === true) return true;
@@ -345,7 +338,7 @@ function formatCreditsLines(credits) {
345
338
  if (credits.used !== undefined || credits.remaining !== undefined) {
346
339
  lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
347
340
  }
348
- if (creditsWereRefunded(credits)) {
341
+ if (creditsRefundedExplicitly(credits)) {
349
342
  lines.push('credits refunded');
350
343
  }
351
344
  return lines;
@@ -360,7 +353,7 @@ function xSearchFailureError(result) {
360
353
  ? ' xAI is unavailable; retry in a few seconds.'
361
354
  : '';
362
355
  const credits = xSearchCredits(result.data);
363
- const refundHint = result.status === 502 && creditsWereRefunded(credits)
356
+ const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
364
357
  ? ' credits refunded.'
365
358
  : '';
366
359
  const lines = [`X search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
@@ -398,6 +391,12 @@ async function runXSearch(options, deps = {}) {
398
391
  forceMint: true,
399
392
  });
400
393
  if (remint?.ok && remint.token) {
394
+ if (!options.json) {
395
+ const print = typeof deps.output === 'function' ? deps.output : () => {};
396
+ for (const line of formatCreditsLines(xSearchCredits(result.data))) {
397
+ print(line);
398
+ }
399
+ }
401
400
  auth = remint;
402
401
  result = await call(auth.token);
403
402
  }