atris 3.57.3 → 3.57.4

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.
@@ -6,11 +6,14 @@ const { spawnSync } = require('child_process');
6
6
  const { hasFlag, readFlag } = require('../lib/arg-parser');
7
7
  const { stampLatestOpenBriefForWorktree } = require('../lib/brief-ledger');
8
8
  const { isConductorArtifact } = require('../lib/conductor-artifacts');
9
+ const close = require('./close');
9
10
 
10
11
  const REGEN_ADAPTER_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
11
12
  const COMMAND_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
12
13
  const GIT_OID_PATTERN = /^[0-9a-f]{40,64}$/;
13
14
  const ONE_LAP_PROOF_REF_PATTERN = /^refs\/atris\/one-lap\/([0-9a-f]{40,64})$/;
15
+ const DAY_MS = 24 * 60 * 60 * 1000;
16
+ const SHIP_HEALTH_SOURCE = 'ship-health:experiments-daily';
14
17
 
15
18
  function runGit(args, { cwd = process.cwd(), check = true, timeout } = {}) {
16
19
  const result = spawnSync('git', args, { cwd, encoding: 'utf8', timeout });
@@ -672,6 +675,56 @@ function findPrimaryRoot(root) {
672
675
  return worktrees[0]?.path || root;
673
676
  }
674
677
 
678
+ function shellQuote(value) {
679
+ const text = String(value || '');
680
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(text)) return text;
681
+ return `'${text.replace(/'/g, `'"'"'`)}'`;
682
+ }
683
+
684
+ function experimentHealthProbe(statePath) {
685
+ const script = [
686
+ "const fs=require('fs')",
687
+ 'const state=JSON.parse(fs.readFileSync(process.argv[1],\'utf8\'))',
688
+ 'const last=Date.parse(state.last_run_date)',
689
+ `process.exit(Number.isFinite(last)&&Date.now()-last<=${DAY_MS}?0:1)`,
690
+ ].join(';');
691
+ return `${shellQuote(process.execPath)} -e ${shellQuote(script)} ${shellQuote(statePath)}`;
692
+ }
693
+
694
+ function runShipHealthCheck(workspaceRoot, options = {}) {
695
+ try {
696
+ const now = options.now ? new Date(options.now) : new Date();
697
+ const statePath = path.join(workspaceRoot, '.atris', 'state', 'experiments-daily.json');
698
+ const state = fs.existsSync(statePath)
699
+ ? JSON.parse(fs.readFileSync(statePath, 'utf8'))
700
+ : {};
701
+ const rawLastRunDate = state && state.last_run_date;
702
+ const lastRunMs = rawLastRunDate ? Date.parse(rawLastRunDate) : null;
703
+ if (rawLastRunDate && !Number.isFinite(lastRunMs)) {
704
+ throw new Error('experiment state date is unreadable');
705
+ }
706
+
707
+ const ageMs = Number.isFinite(lastRunMs) ? now.getTime() - lastRunMs : null;
708
+ if (ageMs !== null && ageMs <= DAY_MS) return { healthy: true, filed: false };
709
+
710
+ const days = ageMs === null ? 1 : Math.max(1, Math.floor(ageMs / DAY_MS));
711
+ console.log(`health check failed: no verified experiment in ${days} days. shipping still works, the metabolism does not.`);
712
+ const filed = close.upsertSourceFlag({
713
+ what: 'no verified experiment has run in over a day',
714
+ owner: 'operator',
715
+ lane: 'code',
716
+ ttlDays: 1,
717
+ source: SHIP_HEALTH_SOURCE,
718
+ closeCondition: 'a verified experiment ran within 24 hours',
719
+ probe: experimentHealthProbe(statePath),
720
+ }, workspaceRoot, { now });
721
+ return { healthy: false, ...filed };
722
+ } catch (error) {
723
+ console.log(`health check skipped: ${String(error && error.message || error).toLowerCase()}. shipping still works.`);
724
+ return { healthy: null, filed: false, error };
725
+ }
726
+ }
727
+
675
728
  function shipWorktree(args) {
676
729
  if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
677
730
  shipHelp();
@@ -825,6 +878,7 @@ function shipWorktree(args) {
825
878
  }
826
879
  }
827
880
  console.log('pr: skipped (local mode)');
881
+ if (merge && !dryRun) runShipHealthCheck(primary || root);
828
882
  console.log('done: worktree shipped');
829
883
  return 0;
830
884
  }
@@ -870,6 +924,7 @@ function shipWorktree(args) {
870
924
  note: `worktree ship completed into ${targetRef}`,
871
925
  }, { worktree: root });
872
926
  } catch {}
927
+ if (merge && !dryRun) runShipHealthCheck(primary || root);
873
928
  console.log('done: worktree shipped');
874
929
  return 0;
875
930
  }
@@ -1219,5 +1274,6 @@ module.exports = {
1219
1274
  taskTokens,
1220
1275
  statusCounts,
1221
1276
  swarloClaim,
1277
+ runShipHealthCheck,
1222
1278
  worktreeCommand,
1223
1279
  };
@@ -1,26 +1,42 @@
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
+ } = require('./youtube');
6
15
 
7
16
  const DEFAULT_TIMEOUT_MS = 120000;
8
17
  const COST_HINT = '5 credits per search';
9
18
  const APPLY_NEXT_MESSAGE =
10
19
  'next: write one apply (change + receipt) for this query.';
20
+ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
11
21
 
12
22
  function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
13
23
  output('');
14
- output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--json]`);
15
- output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--json]`);
24
+ output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json]`);
25
+ output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json]`);
26
+ output(` ${commandName} unsave <query-or-source>`);
16
27
  output('');
17
28
  output(`Search X/Twitter via Atris (${COST_HINT}).`);
18
29
  output('Requires login. Same auth path as atris youtube process.');
30
+ output('Prints to stdout. Rich ephemeral prints one apply next-step (no files).');
31
+ output('--save files a brief only when the result is rich.');
32
+ output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
19
33
  output('Empty or failed search refunds the credits.');
20
34
  output('');
21
35
  output('Options:');
22
36
  output(' --limit <n> Max results hint (search only)');
23
37
  output(' --days <n> Only tweets from the last N days (search only)');
38
+ output(' --save File brief, journal, apply; rich results mint a keep/revert experiment');
39
+ output(' --unsave Delete filed brief, apply stub, and matching experiment pack (no paid calls)');
24
40
  output(' --json Print the raw JSON response');
25
41
  output(' -h, --help This help');
26
42
  output('');
@@ -33,7 +49,10 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
33
49
  output('Examples:');
34
50
  output(` ${commandName} "MCP agents"`);
35
51
  output(` ${commandName} "MCP agents" --limit 5 --days 2`);
52
+ output(` ${commandName} "MCP agents" --save`);
36
53
  output(` ${commandName} person --name "Leah Bonvissuto" --handle leahbon`);
54
+ output(` ${commandName} unsave "MCP agents"`);
55
+ output(` ${commandName} --unsave "MCP agents"`);
37
56
  output('');
38
57
  }
39
58
 
@@ -58,6 +77,8 @@ function parseSearchArgs(argv = []) {
58
77
  mode: 'search',
59
78
  help: false,
60
79
  json: false,
80
+ save: false,
81
+ unsave: false,
61
82
  query: null,
62
83
  limit: null,
63
84
  daysBack: null,
@@ -75,6 +96,10 @@ function parseSearchArgs(argv = []) {
75
96
  options.help = true;
76
97
  } else if (arg === '--json') {
77
98
  options.json = true;
99
+ } else if (arg === '--save') {
100
+ options.save = true;
101
+ } else if (arg === '--unsave') {
102
+ options.unsave = true;
78
103
  } else if (arg === '--limit') {
79
104
  options.limit = parsePositiveInt(readValue(args, i, arg), '--limit');
80
105
  i++;
@@ -111,7 +136,11 @@ function parseSearchArgs(argv = []) {
111
136
  }
112
137
 
113
138
  if (options.help) return options;
114
- if (!options.query) throw new Error('Missing query. Run "atris x-search --help".');
139
+ if (!options.query) {
140
+ throw new Error(options.unsave
141
+ ? 'usage: atris x-search unsave <query-or-source>'
142
+ : 'Missing query. Run "atris x-search --help".');
143
+ }
115
144
  return options;
116
145
  }
117
146
 
@@ -121,6 +150,7 @@ function parsePersonArgs(argv = []) {
121
150
  mode: 'person',
122
151
  help: false,
123
152
  json: false,
153
+ save: false,
124
154
  name: null,
125
155
  handle: null,
126
156
  company: null,
@@ -139,6 +169,8 @@ function parsePersonArgs(argv = []) {
139
169
  options.help = true;
140
170
  } else if (arg === '--json') {
141
171
  options.json = true;
172
+ } else if (arg === '--save') {
173
+ options.save = true;
142
174
  } else if (arg === '--name') {
143
175
  options.name = readValue(args, i, arg);
144
176
  i++;
@@ -184,11 +216,50 @@ function parsePersonArgs(argv = []) {
184
216
  return options;
185
217
  }
186
218
 
219
+ function parseUnsaveArgs(argv = []) {
220
+ const args = [...argv];
221
+ const options = {
222
+ mode: 'unsave',
223
+ help: false,
224
+ source: null,
225
+ };
226
+
227
+ if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
228
+ if (args.length === 0) {
229
+ throw new Error('usage: atris x-search unsave <query-or-source>');
230
+ }
231
+ options.help = true;
232
+ return options;
233
+ }
234
+
235
+ for (let i = 0; i < args.length; i++) {
236
+ const arg = args[i];
237
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
238
+ options.help = true;
239
+ } else if (arg === '--unsave') {
240
+ continue;
241
+ } else if (arg.startsWith('-')) {
242
+ throw new Error(`Unknown option: ${arg}`);
243
+ } else if (!options.source) {
244
+ options.source = arg;
245
+ } else {
246
+ throw new Error(`Unexpected argument: ${arg}`);
247
+ }
248
+ }
249
+
250
+ if (options.help) return options;
251
+ if (!options.source) throw new Error('usage: atris x-search unsave <query-or-source>');
252
+ return options;
253
+ }
254
+
187
255
  function parseXSearchArgs(argv = []) {
188
256
  const args = [...argv];
189
257
  if (args[0] === 'person') {
190
258
  return parsePersonArgs(args.slice(1));
191
259
  }
260
+ if (args[0] === 'unsave') {
261
+ return parseUnsaveArgs(args.slice(1));
262
+ }
192
263
  return parseSearchArgs(args);
193
264
  }
194
265
 
@@ -336,19 +407,138 @@ function xSearchApplyRel(source) {
336
407
  return applyGate.applySidecarRel('x-search', applyGate.applySlug(source));
337
408
  }
338
409
 
410
+ function xSearchExperimentSlug(source) {
411
+ return `x-search-${applyGate.applySlug(source)}`;
412
+ }
413
+
414
+ function xSearchExperimentRel(source) {
415
+ return `atris/experiments/${xSearchExperimentSlug(source)}`;
416
+ }
417
+
418
+ function xSearchBriefRel(source) {
419
+ return `atris/wiki/briefs/x-search-${applyGate.applySlug(source)}.md`;
420
+ }
421
+
339
422
  function xSearchHasResults(data) {
340
423
  return Boolean(xSearchContent(data)) || xSearchCitations(data).length > 0;
341
424
  }
342
425
 
343
- function ensureXSearchApply({ cwd, source, now, output } = {}) {
426
+ function xSearchLessonFromText(text) {
427
+ const body = String(text || '');
428
+ return {
429
+ numbers: extractTeachNumbers(body),
430
+ mechanisms: extractTeachMechanisms(body),
431
+ };
432
+ }
433
+
434
+ function dateStamp(now) {
435
+ if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
436
+ return now.slice(0, 10);
437
+ }
438
+ const value = now instanceof Date ? now : new Date(now || Date.now());
439
+ if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
440
+ return value.toISOString().slice(0, 10);
441
+ }
442
+
443
+ function fileXSearchBrief({ cwd, source, text, now } = {}) {
444
+ try {
445
+ if (!source || !cwd) return null;
446
+ const wikiDir = path.join(cwd, 'atris', 'wiki');
447
+ if (!fs.existsSync(wikiDir)) return null;
448
+ const rel = xSearchBriefRel(source);
449
+ fs.mkdirSync(path.join(cwd, 'atris', 'wiki', 'briefs'), { recursive: true });
450
+ const date = dateStamp(now);
451
+ const heading = String(source).trim().toLowerCase() || 'x search';
452
+ const header = [
453
+ heading,
454
+ '',
455
+ `date: ${date}`,
456
+ `source: ${source}`,
457
+ 'rail: atris x-search',
458
+ ].join('\n');
459
+ fs.writeFileSync(path.join(cwd, rel), `${header}\n\n${String(text || '').trim()}\n`);
460
+
461
+ const journalPath = path.join(cwd, 'atris', 'logs', date.slice(0, 4), `${date}.md`);
462
+ fs.mkdirSync(path.dirname(journalPath), { recursive: true });
463
+ let existing = '';
464
+ if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
465
+ const line = `- [claimable] searched: ${heading} -> ${rel}`;
466
+ if (!existing.includes(line)) {
467
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
468
+ fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
469
+ }
470
+ return rel;
471
+ } catch {
472
+ return null;
473
+ }
474
+ }
475
+
476
+ function removeUnsaveRel(cwd, rel, removed) {
477
+ const abs = path.join(cwd, rel);
478
+ try {
479
+ if (!fs.existsSync(abs)) return;
480
+ const st = fs.lstatSync(abs);
481
+ if (st.isDirectory()) fs.rmSync(abs, { recursive: true, force: true });
482
+ else fs.unlinkSync(abs);
483
+ removed.push(rel);
484
+ } catch {
485
+ // already gone or unreadable: do not error
486
+ }
487
+ }
488
+
489
+ function unsaveXSearch(target, deps = {}) {
490
+ const output = deps.output || ((line = '') => console.log(line));
491
+ const cwd = deps.cwd || process.cwd();
492
+ const source = String(target || '').trim();
493
+ if (!source) {
494
+ output('usage: atris x-search unsave <query-or-source>');
495
+ return 2;
496
+ }
497
+ const briefRel = xSearchBriefRel(source);
498
+ const applyRel = xSearchApplyRel(source);
499
+ const packRel = xSearchExperimentRel(source);
500
+ const removed = [];
501
+ for (const rel of [briefRel, applyRel, packRel]) {
502
+ removeUnsaveRel(cwd, rel, removed);
503
+ }
504
+ if (!removed.length) {
505
+ output(`already gone: ${briefRel} and ${applyRel}`);
506
+ return 0;
507
+ }
508
+ output(`removed ${removed.join(' and ')}`);
509
+ return 0;
510
+ }
511
+
512
+ function saveRichXSearch({ cwd, source, text, now } = {}) {
513
+ const lesson = xSearchLessonFromText(text);
514
+ if (isThinTeachLesson(lesson)) {
515
+ return { thin: true, brief: null, packRel: null };
516
+ }
517
+ const brief = fileXSearchBrief({ cwd, source, text, now });
518
+ const packRel = fileTeachExperiment({
519
+ cwd,
520
+ lesson,
521
+ slug: source ? xSearchExperimentSlug(source) : null,
522
+ applyRel: source ? xSearchApplyRel(source) : null,
523
+ });
524
+ return { thin: false, brief, packRel };
525
+ }
526
+
527
+ function ensureXSearchApply({ cwd, source, packRel, now, output } = {}) {
528
+ const pack = packRel || (source ? xSearchExperimentRel(source) : null);
344
529
  return applyGate.ensureApply({
345
530
  cwd,
346
531
  source,
347
532
  rel: source ? xSearchApplyRel(source) : null,
348
533
  now,
349
534
  output,
350
- incompleteMessage: APPLY_NEXT_MESSAGE,
535
+ incompleteMessage: pack
536
+ ? `next: apply ${pack}. keep only if measure.py moves 0→1`
537
+ : APPLY_NEXT_MESSAGE,
351
538
  required: false,
539
+ change: pack ? `apply ${pack}` : undefined,
540
+ receipt: pack ? KEEP_RULE : undefined,
541
+ journalLine: pack ? `- [claimable] apply: ${pack}. ${KEEP_RULE}` : undefined,
352
542
  });
353
543
  }
354
544
 
@@ -407,6 +597,10 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
407
597
  return 0;
408
598
  }
409
599
 
600
+ if (options.mode === 'unsave' || options.unsave) {
601
+ return unsaveXSearch(options.source || options.query, deps);
602
+ }
603
+
410
604
  let status = 0;
411
605
  try {
412
606
  const data = await runXSearch(options, deps);
@@ -416,16 +610,37 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
416
610
  } else {
417
611
  output(hasResults ? formatXSearchResult(data) : formatEmptyXSearchResult(data));
418
612
  }
419
- if (hasResults) {
420
- const ensureApply = deps.ensureApply || ensureXSearchApply;
421
- status = ensureApply({
613
+ if (!hasResults) {
614
+ status = 2;
615
+ } else if (options.save) {
616
+ const source = xSearchApplySource(options);
617
+ const saved = saveRichXSearch({
422
618
  cwd: deps.cwd || process.cwd(),
423
- source: xSearchApplySource(options),
424
- now: deps.applyNow,
425
- output,
619
+ source,
620
+ text: xSearchContent(data),
621
+ now: deps.applyNow || deps.now,
426
622
  });
623
+ if (saved.thin) {
624
+ output(TEACH_THIN_REFUSE);
625
+ status = 2;
626
+ } else {
627
+ const ensureApply = deps.ensureApply || ensureXSearchApply;
628
+ status = ensureApply({
629
+ cwd: deps.cwd || process.cwd(),
630
+ source,
631
+ packRel: saved.packRel,
632
+ now: deps.applyNow || deps.now,
633
+ output,
634
+ });
635
+ }
427
636
  } else {
428
- status = 2;
637
+ if (
638
+ !options.json
639
+ && !isThinTeachLesson(xSearchLessonFromText(xSearchContent(data)))
640
+ ) {
641
+ applyGate.hintEphemeralApply(output, 'x-search');
642
+ }
643
+ status = 0;
429
644
  }
430
645
  } catch (err) {
431
646
  output(err.message);
@@ -439,12 +654,14 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
439
654
 
440
655
  module.exports = {
441
656
  DEFAULT_TIMEOUT_MS,
442
- APPLY_NEXT_MESSAGE,
443
657
  parseXSearchArgs,
444
658
  buildSearchPayload,
445
659
  buildPersonPayload,
446
- formatXSearchResult,
447
660
  xSearchHasResults,
448
661
  xSearchApplyRel,
662
+ xSearchBriefRel,
663
+ xSearchExperimentSlug,
664
+ xSearchExperimentRel,
665
+ unsaveXSearch,
449
666
  xSearchCommand,
450
667
  };