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
@@ -1,26 +1,44 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
3
5
  const { apiRequestJson } = require('../utils/api');
4
6
  const { ensureBilledCommandAuth } = require('./auth');
5
7
  const applyGate = require('../lib/apply-gate');
8
+ const {
9
+ fileTeachExperiment,
10
+ extractTeachNumbers,
11
+ extractTeachMechanisms,
12
+ isThinTeachLesson,
13
+ TEACH_THIN_REFUSE,
14
+ printLearnerCheckGate,
15
+ proveSavedLearnerBaseline,
16
+ } = require('./youtube');
6
17
 
7
18
  const DEFAULT_TIMEOUT_MS = 120000;
8
19
  const COST_HINT = '5 credits per search';
9
20
  const APPLY_NEXT_MESSAGE =
10
21
  'next: write one apply (change + receipt) for this query.';
22
+ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
11
23
 
12
24
  function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
13
25
  output('');
14
- output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--json]`);
15
- output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--json]`);
26
+ output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json]`);
27
+ output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json]`);
28
+ output(` ${commandName} unsave <query-or-source>`);
16
29
  output('');
17
30
  output(`Search X/Twitter via Atris (${COST_HINT}).`);
18
31
  output('Requires login. Same auth path as atris youtube process.');
32
+ output('Prints to stdout. Rich ephemeral prints one apply next-step, then hands off to atris youtube search (no files).');
33
+ output('--save files a brief only when the result is rich.');
34
+ output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
19
35
  output('Empty or failed search refunds the credits.');
20
36
  output('');
21
37
  output('Options:');
22
38
  output(' --limit <n> Max results hint (search only)');
23
39
  output(' --days <n> Only tweets from the last N days (search only)');
40
+ output(' --save File brief, journal, apply; rich results mint a keep/revert experiment');
41
+ output(' --unsave Delete filed brief, apply stub, and matching experiment pack (no paid calls)');
24
42
  output(' --json Print the raw JSON response');
25
43
  output(' -h, --help This help');
26
44
  output('');
@@ -33,7 +51,10 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
33
51
  output('Examples:');
34
52
  output(` ${commandName} "MCP agents"`);
35
53
  output(` ${commandName} "MCP agents" --limit 5 --days 2`);
54
+ output(` ${commandName} "MCP agents" --save`);
36
55
  output(` ${commandName} person --name "Leah Bonvissuto" --handle leahbon`);
56
+ output(` ${commandName} unsave "MCP agents"`);
57
+ output(` ${commandName} --unsave "MCP agents"`);
37
58
  output('');
38
59
  }
39
60
 
@@ -58,6 +79,8 @@ function parseSearchArgs(argv = []) {
58
79
  mode: 'search',
59
80
  help: false,
60
81
  json: false,
82
+ save: false,
83
+ unsave: false,
61
84
  query: null,
62
85
  limit: null,
63
86
  daysBack: null,
@@ -75,6 +98,10 @@ function parseSearchArgs(argv = []) {
75
98
  options.help = true;
76
99
  } else if (arg === '--json') {
77
100
  options.json = true;
101
+ } else if (arg === '--save') {
102
+ options.save = true;
103
+ } else if (arg === '--unsave') {
104
+ options.unsave = true;
78
105
  } else if (arg === '--limit') {
79
106
  options.limit = parsePositiveInt(readValue(args, i, arg), '--limit');
80
107
  i++;
@@ -111,7 +138,11 @@ function parseSearchArgs(argv = []) {
111
138
  }
112
139
 
113
140
  if (options.help) return options;
114
- if (!options.query) throw new Error('Missing query. Run "atris x-search --help".');
141
+ if (!options.query) {
142
+ throw new Error(options.unsave
143
+ ? 'usage: atris x-search unsave <query-or-source>'
144
+ : 'Missing query. Run "atris x-search --help".');
145
+ }
115
146
  return options;
116
147
  }
117
148
 
@@ -121,6 +152,7 @@ function parsePersonArgs(argv = []) {
121
152
  mode: 'person',
122
153
  help: false,
123
154
  json: false,
155
+ save: false,
124
156
  name: null,
125
157
  handle: null,
126
158
  company: null,
@@ -139,6 +171,8 @@ function parsePersonArgs(argv = []) {
139
171
  options.help = true;
140
172
  } else if (arg === '--json') {
141
173
  options.json = true;
174
+ } else if (arg === '--save') {
175
+ options.save = true;
142
176
  } else if (arg === '--name') {
143
177
  options.name = readValue(args, i, arg);
144
178
  i++;
@@ -184,11 +218,53 @@ function parsePersonArgs(argv = []) {
184
218
  return options;
185
219
  }
186
220
 
221
+ function parseUnsaveArgs(argv = []) {
222
+ const args = [...argv];
223
+ const options = {
224
+ mode: 'unsave',
225
+ help: false,
226
+ json: false,
227
+ source: null,
228
+ sources: [],
229
+ };
230
+
231
+ if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
232
+ if (args.length === 0) {
233
+ throw new Error('usage: atris x-search unsave <query-or-source>');
234
+ }
235
+ options.help = true;
236
+ return options;
237
+ }
238
+
239
+ for (let i = 0; i < args.length; i++) {
240
+ const arg = args[i];
241
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
242
+ options.help = true;
243
+ } else if (arg === '--json') {
244
+ options.json = true;
245
+ } else if (arg === '--unsave') {
246
+ continue;
247
+ } else if (arg.startsWith('-')) {
248
+ throw new Error(`Unknown option: ${arg}`);
249
+ } else {
250
+ options.sources.push(arg);
251
+ if (!options.source) options.source = arg;
252
+ }
253
+ }
254
+
255
+ if (options.help) return options;
256
+ if (!options.sources.length) throw new Error('usage: atris x-search unsave <query-or-source>');
257
+ return options;
258
+ }
259
+
187
260
  function parseXSearchArgs(argv = []) {
188
261
  const args = [...argv];
189
262
  if (args[0] === 'person') {
190
263
  return parsePersonArgs(args.slice(1));
191
264
  }
265
+ if (args[0] === 'unsave') {
266
+ return parseUnsaveArgs(args.slice(1));
267
+ }
192
268
  return parseSearchArgs(args);
193
269
  }
194
270
 
@@ -258,6 +334,12 @@ function creditsWereRefunded(credits) {
258
334
  return typeof credits.refunded === 'number' && credits.refunded > 0;
259
335
  }
260
336
 
337
+ function creditsRefundedExplicitly(credits) {
338
+ if (!credits) return false;
339
+ if (credits.refunded === true) return true;
340
+ return typeof credits.refunded === 'number' && credits.refunded > 0;
341
+ }
342
+
261
343
  function formatCreditsLines(credits) {
262
344
  const lines = [];
263
345
  if (credits.used !== undefined || credits.remaining !== undefined) {
@@ -332,23 +414,182 @@ function xSearchApplySource(options) {
332
414
  return options?.query;
333
415
  }
334
416
 
417
+ function quoteYoutubeSearchQuery(source) {
418
+ return `"${String(source || '').replace(/"/g, '')}"`;
419
+ }
420
+
421
+ function printYoutubeSearchNext(source, output) {
422
+ if (!source) return;
423
+ output(`next: atris youtube search ${quoteYoutubeSearchQuery(source)}`);
424
+ }
425
+
426
+ const EMPTY_YOUTUBE_SEARCH_NEXT = 'next: atris youtube search " "';
427
+
428
+ function printEmptyYoutubeSearchNext(output) {
429
+ output(EMPTY_YOUTUBE_SEARCH_NEXT);
430
+ }
431
+
335
432
  function xSearchApplyRel(source) {
336
433
  return applyGate.applySidecarRel('x-search', applyGate.applySlug(source));
337
434
  }
338
435
 
436
+ function xSearchExperimentSlug(source) {
437
+ return `x-search-${applyGate.applySlug(source)}`;
438
+ }
439
+
440
+ function xSearchExperimentRel(source) {
441
+ return `atris/experiments/${xSearchExperimentSlug(source)}`;
442
+ }
443
+
444
+ function xSearchBriefRel(source) {
445
+ return `atris/wiki/briefs/x-search-${applyGate.applySlug(source)}.md`;
446
+ }
447
+
339
448
  function xSearchHasResults(data) {
340
449
  return Boolean(xSearchContent(data)) || xSearchCitations(data).length > 0;
341
450
  }
342
451
 
343
- function ensureXSearchApply({ cwd, source, now, output } = {}) {
452
+ function xSearchLessonFromText(text) {
453
+ const body = String(text || '');
454
+ return {
455
+ numbers: extractTeachNumbers(body),
456
+ mechanisms: extractTeachMechanisms(body),
457
+ };
458
+ }
459
+
460
+ function dateStamp(now) {
461
+ if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
462
+ return now.slice(0, 10);
463
+ }
464
+ const value = now instanceof Date ? now : new Date(now || Date.now());
465
+ if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
466
+ return value.toISOString().slice(0, 10);
467
+ }
468
+
469
+ function fileXSearchBrief({ cwd, source, text, now } = {}) {
470
+ try {
471
+ if (!source || !cwd) return null;
472
+ const wikiDir = path.join(cwd, 'atris', 'wiki');
473
+ if (!fs.existsSync(wikiDir)) return null;
474
+ const rel = xSearchBriefRel(source);
475
+ fs.mkdirSync(path.join(cwd, 'atris', 'wiki', 'briefs'), { recursive: true });
476
+ const date = dateStamp(now);
477
+ const heading = String(source).trim().toLowerCase() || 'x search';
478
+ const header = [
479
+ heading,
480
+ '',
481
+ `date: ${date}`,
482
+ `source: ${source}`,
483
+ 'rail: atris x-search',
484
+ ].join('\n');
485
+ fs.writeFileSync(path.join(cwd, rel), `${header}\n\n${String(text || '').trim()}\n`);
486
+
487
+ const journalPath = path.join(cwd, 'atris', 'logs', date.slice(0, 4), `${date}.md`);
488
+ fs.mkdirSync(path.dirname(journalPath), { recursive: true });
489
+ let existing = '';
490
+ if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
491
+ const line = `- [claimable] searched: ${heading} -> ${rel}`;
492
+ if (!existing.includes(line)) {
493
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
494
+ fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
495
+ }
496
+ return rel;
497
+ } catch {
498
+ return null;
499
+ }
500
+ }
501
+
502
+ function removeUnsaveRel(cwd, rel, removed) {
503
+ const abs = path.join(cwd, rel);
504
+ try {
505
+ if (!fs.existsSync(abs)) return;
506
+ const st = fs.lstatSync(abs);
507
+ if (st.isDirectory()) fs.rmSync(abs, { recursive: true, force: true });
508
+ else fs.unlinkSync(abs);
509
+ removed.push(rel);
510
+ } catch {
511
+ // already gone or unreadable: do not error
512
+ }
513
+ }
514
+
515
+ function unsaveXSearch(target, deps = {}) {
516
+ const output = deps.output || ((line = '') => console.log(line));
517
+ const cwd = deps.cwd || process.cwd();
518
+ const source = String(target || '').trim();
519
+ if (!source) {
520
+ output('usage: atris x-search unsave <query-or-source>');
521
+ return 2;
522
+ }
523
+ const briefRel = xSearchBriefRel(source);
524
+ const applyRel = xSearchApplyRel(source);
525
+ const packRel = xSearchExperimentRel(source);
526
+ const removed = [];
527
+ for (const rel of [briefRel, applyRel, packRel]) {
528
+ removeUnsaveRel(cwd, rel, removed);
529
+ }
530
+ if (!removed.length) {
531
+ output(`already gone: ${briefRel} and ${applyRel}`);
532
+ return 0;
533
+ }
534
+ output(`removed ${removed.join(' and ')}`);
535
+ return 0;
536
+ }
537
+
538
+ function unsaveTargets(options) {
539
+ if (Array.isArray(options?.sources) && options.sources.length) return options.sources;
540
+ const single = options?.source || options?.query;
541
+ return single ? [single] : [];
542
+ }
543
+
544
+ function runXSearchUnsave(options, deps = {}) {
545
+ const output = deps.output || ((line = '') => console.log(line));
546
+ const targets = unsaveTargets(options);
547
+ if (!targets.length) {
548
+ output('usage: atris x-search unsave <query-or-source>');
549
+ return 2;
550
+ }
551
+ let code = 0;
552
+ for (const target of targets) {
553
+ const status = unsaveXSearch(target, deps);
554
+ if (status !== 0) code = status;
555
+ }
556
+ if (code === 0 && options.json !== true && deps.json !== true) {
557
+ printEmptyYoutubeSearchNext(output);
558
+ }
559
+ return code;
560
+ }
561
+
562
+ function saveRichXSearch({ cwd, source, text, now } = {}) {
563
+ const lesson = xSearchLessonFromText(text);
564
+ if (isThinTeachLesson(lesson)) {
565
+ return { thin: true, brief: null, packRel: null, lesson };
566
+ }
567
+ const brief = fileXSearchBrief({ cwd, source, text, now });
568
+ const packRel = fileTeachExperiment({
569
+ cwd,
570
+ lesson,
571
+ slug: source ? xSearchExperimentSlug(source) : null,
572
+ applyRel: source ? xSearchApplyRel(source) : null,
573
+ });
574
+ return { thin: false, brief, packRel, lesson };
575
+ }
576
+
577
+ function ensureXSearchApply({ cwd, source, packRel, now, output } = {}) {
578
+ const pack = packRel || (source ? xSearchExperimentRel(source) : null);
579
+ const slug = pack ? path.basename(pack) : null;
344
580
  return applyGate.ensureApply({
345
581
  cwd,
346
582
  source,
347
583
  rel: source ? xSearchApplyRel(source) : null,
348
584
  now,
349
585
  output,
350
- incompleteMessage: APPLY_NEXT_MESSAGE,
586
+ incompleteMessage: slug
587
+ ? `next: atris experiments keep ${slug}`
588
+ : APPLY_NEXT_MESSAGE,
351
589
  required: false,
590
+ change: pack ? `apply ${pack}` : undefined,
591
+ receipt: pack ? KEEP_RULE : undefined,
592
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${KEEP_RULE}` : undefined,
352
593
  });
353
594
  }
354
595
 
@@ -407,6 +648,10 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
407
648
  return 0;
408
649
  }
409
650
 
651
+ if (options.mode === 'unsave' || options.unsave) {
652
+ return runXSearchUnsave(options, deps);
653
+ }
654
+
410
655
  let status = 0;
411
656
  try {
412
657
  const data = await runXSearch(options, deps);
@@ -416,16 +661,58 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
416
661
  } else {
417
662
  output(hasResults ? formatXSearchResult(data) : formatEmptyXSearchResult(data));
418
663
  }
419
- if (hasResults) {
420
- const ensureApply = deps.ensureApply || ensureXSearchApply;
421
- status = ensureApply({
422
- cwd: deps.cwd || process.cwd(),
423
- source: xSearchApplySource(options),
424
- now: deps.applyNow,
425
- output,
664
+ if (!hasResults) {
665
+ if (
666
+ !options.json
667
+ && !options.save
668
+ && !creditsRefundedExplicitly(xSearchCredits(data))
669
+ ) {
670
+ printEmptyYoutubeSearchNext(output);
671
+ }
672
+ status = 2;
673
+ } else if (options.save) {
674
+ const source = xSearchApplySource(options);
675
+ const cwd = deps.cwd || process.cwd();
676
+ const saved = saveRichXSearch({
677
+ cwd,
678
+ source,
679
+ text: xSearchContent(data),
680
+ now: deps.applyNow || deps.now,
426
681
  });
682
+ if (saved.thin) {
683
+ output(TEACH_THIN_REFUSE);
684
+ status = 2;
685
+ } else {
686
+ const ensureApply = deps.ensureApply || ensureXSearchApply;
687
+ const applyCode = ensureApply({
688
+ cwd,
689
+ source,
690
+ packRel: saved.packRel,
691
+ now: deps.applyNow || deps.now,
692
+ output,
693
+ });
694
+ if (deps.ensureApply) {
695
+ status = applyCode;
696
+ } else {
697
+ const baseline = proveSavedLearnerBaseline({
698
+ cwd,
699
+ applyRel: source ? xSearchApplyRel(source) : null,
700
+ lesson: saved.lesson,
701
+ output,
702
+ json: options.json === true,
703
+ });
704
+ status = baseline !== 0 ? baseline : applyCode;
705
+ }
706
+ }
427
707
  } else {
428
- status = 2;
708
+ const lesson = xSearchLessonFromText(xSearchContent(data));
709
+ const json = options.json === true;
710
+ if (!json && !isThinTeachLesson(lesson)) {
711
+ applyGate.hintEphemeralApply(output, 'x-search');
712
+ }
713
+ printLearnerCheckGate(output, lesson, { includeCheck: true, json });
714
+ if (!json) printYoutubeSearchNext(xSearchApplySource(options), output);
715
+ status = 0;
429
716
  }
430
717
  } catch (err) {
431
718
  output(err.message);
@@ -439,12 +726,14 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
439
726
 
440
727
  module.exports = {
441
728
  DEFAULT_TIMEOUT_MS,
442
- APPLY_NEXT_MESSAGE,
443
729
  parseXSearchArgs,
444
730
  buildSearchPayload,
445
731
  buildPersonPayload,
446
- formatXSearchResult,
447
732
  xSearchHasResults,
448
733
  xSearchApplyRel,
734
+ xSearchBriefRel,
735
+ xSearchExperimentSlug,
736
+ xSearchExperimentRel,
737
+ unsaveXSearch,
449
738
  xSearchCommand,
450
739
  };
package/commands/xp.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const crypto = require('crypto');
7
7
  const { spawnSync } = require('child_process');
8
8
  const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
9
+ const { treeHashFor } = require('../lib/tree-hash');
9
10
 
10
11
  const DEFAULT_GRAPH_DAYS = 365;
11
12
  const MAX_SYNC_GRAPH_DAYS = 370;
@@ -22,7 +23,7 @@ const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
22
23
 
23
24
  // Default single-workspace XP root. Local XP reads task_episodes.jsonl (written
24
25
  // by task-db at the resolved workspace root) and writes its projection beside
25
- // it, so the `--workspace` default must resolve the same shared root otherwise
26
+ // it, so the `--workspace` default must resolve the same shared root, otherwise
26
27
  // `xp --local` from a subdir (e.g. backend/) read the wrong/empty episodes and
27
28
  // split the projection into a nested .atris. Explicit --workspace still wins;
28
29
  // the multi-root roster path (discoverCareerXpWorkspaces) is untouched. Falls
@@ -908,7 +909,7 @@ function readTaskEpisodeTail(episodePath, cursorPath, options = {}) {
908
909
  }
909
910
  }
910
911
 
911
- function receiptFromTaskEpisode(episode) {
912
+ function receiptFromTaskEpisode(episode, treeHash) {
912
913
  const episodeId = episode?.episode_id;
913
914
  const label = episode?.rl?.label;
914
915
  const rejected = label === 'rework_requested' || label === 'rejected';
@@ -924,6 +925,7 @@ function receiptFromTaskEpisode(episode) {
924
925
 
925
926
  return {
926
927
  schema: 'atris.career_xp_receipt.v1',
928
+ tree_hash: treeHash,
927
929
  receipt_id: `task_review:${episodeId}${rejected ? ':rejected' : ''}`,
928
930
  source: 'atris-cli',
929
931
  source_type: 'task_review',
@@ -1071,10 +1073,11 @@ function collectLocalXpProjectionState(args = [], { write = true } = {}) {
1071
1073
  && fileSize(episodePath) > 0
1072
1074
  && fileSize(receiptsPath) === 0;
1073
1075
  const tail = readTaskEpisodeTail(episodePath, cursorPath, { forceReset: replayFromStart });
1076
+ const treeHash = write && tail.episodes.length ? treeHashFor(workspace) : null;
1074
1077
  const seen = new Set(existingChain.receipts.map(receipt => receipt.receipt_id).filter(Boolean));
1075
1078
  let previousHash = existingChain.integrity.head_hash || null;
1076
1079
  const newReceipts = tail.episodes
1077
- .map(receiptFromTaskEpisode)
1080
+ .map((episode) => receiptFromTaskEpisode(episode, treeHash))
1078
1081
  .filter(Boolean)
1079
1082
  .filter((receipt) => {
1080
1083
  if (seen.has(receipt.receipt_id)) return false;