atris 3.46.1 → 3.48.0

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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: design
3
3
  description: Frontend aesthetics policy. Use when building UI, components, landing pages, dashboards, or any frontend work. Prevents generic ai-generated look.
4
- version: 3.1.0
4
+ version: 3.1.1
5
5
  allowed-tools: Read, Write, Edit, Bash, Glob
6
6
  tags:
7
7
  - design
@@ -112,6 +112,7 @@ Every entry: id, rule, detector, status. A detector is a regex/command a gate ca
112
112
  | D11 | layout contracts, not dioramas: min-height over fixed height, fluid max-width over fixed px, every overflow reachable (scroll or +N more), stress-test with hostile content before shipping | judgment | active |
113
113
  | D12 | one accent moment per card: brand accent for the primary action only, gold for confidence/progress fills, green only for completed; everything else tonal | judgment | active |
114
114
  | D13 | motion is calm and eased: 120-300ms ease-out on opacity/transform only, loops match a measured source cadence, prefers-reduced-motion always freezes them | judgment | active |
115
+ | D14 | compact selectors lead with the chosen name and a discriminating icon; remove redundant field labels and visible type explanations when icon, title, and accessible label carry them | judgment | active |
115
116
 
116
117
  Measured recipes live in the mimic studies: `~/arena/mimic-beautiful-ui/LESSONS.md` (18 AI-interface components with exact tokens) and its `remix.css` :root block (the portable Atris token sheet, coffee + paper themes). Start there before designing an agent surface.
117
118
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: youtube
3
- description: "Process YouTube videos extract insights, answer questions, store as knowledge. 5 credits per video. Triggers on: youtube, video, process video, watch this, learn from video."
3
+ description: "Process YouTube videos: extract insights, answer questions, store as knowledge. 5 credits per video. Triggers on: youtube, video, process video, watch this, learn from video."
4
4
  version: 2.3.0
5
5
  tags:
6
6
  - youtube
@@ -15,9 +15,11 @@ Process any YouTube video through Atris transcript-first analysis. The CLI extra
15
15
 
16
16
  ## Route first: learning vs product
17
17
 
18
- Two rails process YouTube videos pick before running anything:
18
+ Two rails process YouTube videos: pick before running anything:
19
19
 
20
20
  - **Learning / work rail** → use the `alpha-learn` skill (ytnotes). Local yt-dlp + grok, zero credits, podcastnotes-style notes, tweet-feed output, `[claimable]` entries in today's journal for other agents. Use this when the goal is to LEARN from a video or mine it for Atris work.
21
+ Canonical ytnotes source is `scripts/det/ytnotes` (install: `ln -sf "$PWD/scripts/det/ytnotes" ~/.local/bin/ytnotes`).
22
+ Runs are scored by `node scripts/det/ytrail-eval.js`.
21
23
  - **Product rail** → this skill (`atris youtube process`). Credits-billed, stores knowledge in the Atris backend, customer-facing path. Use this when a customer/agent needs the video stored as Atris knowledge or answered via the API.
22
24
 
23
25
  If the user says "learn from", "notes on", "alpha", or "rabbit hole" → alpha-learn. If they say "process", "store", "add to knowledge" → this skill.
@@ -189,7 +191,7 @@ Every important insight should carry a timestamp when the transcript provides on
189
191
  | `401` | Token expired/invalid | `atris login --force` |
190
192
  | `402` | Not enough credits | Check balance, purchase at atris.ai |
191
193
  | `400` | Invalid YouTube URL | Check URL format |
192
- | `502` | Transcript or cloud processing failed | Retry credits auto-refunded when backend fails |
194
+ | `502` | Transcript or cloud processing failed | Retry; credits auto-refunded when backend fails |
193
195
 
194
196
  ---
195
197
 
package/bin/atris.js CHANGED
@@ -38,6 +38,7 @@ try {
38
38
  const {
39
39
  checkForUpdates,
40
40
  showUpdateNotification,
41
+ autoUpdate,
41
42
  inspectInstallGitState,
42
43
  formatInstallGitWarning,
43
44
  } = require('../utils/update-check');
@@ -106,6 +107,7 @@ if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'updat
106
107
  showUpdateNotification(updateInfo, {
107
108
  packageRoot: path.join(__dirname, '..'),
108
109
  });
110
+ autoUpdate(updateInfo, { packageRoot: path.join(__dirname, '..') });
109
111
  }
110
112
  return updateInfo;
111
113
  })
@@ -569,7 +571,7 @@ function showHelp() {
569
571
  console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
570
572
  console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
571
573
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
572
- console.log(' youtube - Process YouTube videos with timestamped transcript-first analysis');
574
+ console.log(' youtube - Free local notes or 5-credit cloud process for YouTube videos');
573
575
  console.log('');
574
576
  console.log('Experiments:');
575
577
  console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
package/commands/ci.js CHANGED
@@ -14,7 +14,7 @@ function showCiHelp() {
14
14
  console.log('atris ci runs github actions jobs on this machine with a warm local work folder.');
15
15
  console.log('change runs-on: ubuntu-latest to runs-on: atris.');
16
16
  console.log('a GITHUB_TOKEN or an authenticated gh cli is required.');
17
- console.log('usage: atris ci runner --repo <owner/name> [--label <name>] [--once]');
17
+ console.log('usage: atris ci runner --repo <owner/name> [--repo <owner/name> ...] [--label <name>] [--once]');
18
18
  console.log(' atris ci usage [--repo <owner/name>]');
19
19
  }
20
20
 
package/commands/close.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const crypto = require('crypto');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const { spawnSync } = require('child_process');
4
5
  const { knownCommands } = require('../lib/known-commands');
5
6
  const { readUsage, usagePath } = require('../lib/usage');
6
7
  const pulse = require('../lib/pulse');
@@ -16,6 +17,12 @@ const EXPERIMENTS_RELATIVE_PATH = path.join('.atris', 'state', 'experiments.json
16
17
  const EXPERIMENTS_DIR_RELATIVE_PATH = path.join('atris', 'experiments');
17
18
  const RESOLVED_IN_SOURCE_PROOF = 'resolved in source store';
18
19
  const FAILED_TASK_BATCH_PREFIX = 'tasks:failed:';
20
+ // Live-probe incidents: a diagnosis may only be recorded alongside a command
21
+ // that fails right now, and may only close after that command passes twice
22
+ // with a real gap between runs (catches the bug that comes back hours later).
23
+ const PROBE_TIMEOUT_MS = 60 * 1000;
24
+ const PROBE_HOLD_GAP_MS = 60 * 60 * 1000;
25
+ const PROBE_STOP_AFTER_FAILED_CHECKS = 2;
19
26
 
20
27
  function ledgerPath(cwd = process.cwd()) {
21
28
  return path.join(cwd, LEDGER_RELATIVE_PATH);
@@ -111,6 +118,8 @@ function foldEvents(events, options = {}) {
111
118
  ttl_days: Number(event.ttl_days) || 7,
112
119
  close_condition: normalizeSpaces(event.close_condition || 'the loop is resolved'),
113
120
  source: normalizeSpaces(event.source || 'manual'),
121
+ probe: normalizeSpaces(event.probe || '') || null,
122
+ probe_runs: [],
114
123
  status: 'open',
115
124
  closed_at: null,
116
125
  dissolved_at: null,
@@ -138,6 +147,12 @@ function foldEvents(events, options = {}) {
138
147
  } else if (event.kind === 'escalated') {
139
148
  const day = event.day || (event.at ? String(event.at).slice(0, 10) : null);
140
149
  if (day && !flag.escalated_days.includes(day)) flag.escalated_days.push(day);
150
+ } else if (event.kind === 'probed') {
151
+ flag.probe_runs.push({
152
+ at: eventTime(event),
153
+ pass: !!event.pass,
154
+ exit_code: Number.isFinite(event.exit_code) ? event.exit_code : null,
155
+ });
141
156
  }
142
157
  }
143
158
 
@@ -161,9 +176,49 @@ function withComputedState(flag, now = new Date()) {
161
176
  days_old: daysOld,
162
177
  days_past_ttl: daysPastTtl,
163
178
  overdue,
179
+ ...probeState(flag),
164
180
  };
165
181
  }
166
182
 
183
+ // Probe state from recorded runs. 'failing' until a check passes, 'passed
184
+ // once' until a second pass lands PROBE_HOLD_GAP_MS later, then 'holding'
185
+ // (eligible to close). failed_checks counts re-checks that still failed:
186
+ // each one is a fix attempt that did not move reality.
187
+ function probeState(flag) {
188
+ if (!flag.probe) {
189
+ return { probe_status: null, failed_checks: 0, probe_holding: false };
190
+ }
191
+ const runs = flag.probe_runs || [];
192
+ const failedChecks = runs.filter((run) => !run.pass).length;
193
+
194
+ const trailing = [];
195
+ for (let index = runs.length - 1; index >= 0 && runs[index].pass; index -= 1) {
196
+ trailing.unshift(runs[index]);
197
+ }
198
+
199
+ let status = 'failing';
200
+ let holding = false;
201
+ if (trailing.length >= 1) {
202
+ const firstMs = parseDate(trailing[0].at) || 0;
203
+ const lastMs = parseDate(trailing[trailing.length - 1].at) || 0;
204
+ holding = trailing.length >= 2 && lastMs - firstMs >= PROBE_HOLD_GAP_MS;
205
+ status = holding ? 'holding' : 'passed once';
206
+ }
207
+ return { probe_status: status, failed_checks: failedChecks, probe_holding: holding };
208
+ }
209
+
210
+ function runProbe(command, context = {}) {
211
+ const result = spawnSync(command, {
212
+ shell: true,
213
+ cwd: context.cwd || process.cwd(),
214
+ timeout: PROBE_TIMEOUT_MS,
215
+ stdio: ['ignore', 'pipe', 'pipe'],
216
+ encoding: 'utf8',
217
+ });
218
+ const exitCode = typeof result.status === 'number' ? result.status : 1;
219
+ return { pass: exitCode === 0, exit_code: exitCode };
220
+ }
221
+
167
222
  function openFlags(cwd = process.cwd(), options = {}) {
168
223
  return foldEvents(readEvents(cwd), options).filter((flag) => flag.status === 'open');
169
224
  }
@@ -202,6 +257,7 @@ function listLine(flag) {
202
257
  flag.what.toLowerCase(),
203
258
  `${formatDays(flag.days_old)} old`,
204
259
  ];
260
+ if (flag.probe) parts.push(`probe ${flag.probe_status}`);
205
261
  if (flag.overdue) {
206
262
  parts.push(`${formatDays(flag.days_past_ttl)} past ttl`);
207
263
  }
@@ -269,10 +325,11 @@ function publicFlag(flag) {
269
325
  }
270
326
 
271
327
  function printHelp() {
272
- console.log('usage: atris close <add|list|done|dissolve|snooze|sweep|scan>');
273
- console.log('add "<what>" [--owner x] [--lane life|business|code] [--ttl 7] [--when "<condition>"] [--source y]');
328
+ console.log('usage: atris close <add|list|done|dissolve|snooze|sweep|scan|check>');
329
+ console.log('add "<what>" [--owner x] [--lane life|business|code] [--ttl 7] [--when "<condition>"] [--source y] [--probe "<cmd that fails now>"]');
274
330
  console.log('list [--json] [--lane x]');
275
- console.log('done <id> [--proof "..."]');
331
+ console.log('done <id> [--proof "..."] (a probed incident closes only after its probe holds)');
332
+ console.log('check [id] [--json] (run live probes; failing probe = old theory still live)');
276
333
  console.log('dissolve <id> --why "..."');
277
334
  console.log('snooze <id> --days n');
278
335
  console.log('sweep [--json]');
@@ -295,6 +352,20 @@ function commandAdd(args, context = {}) {
295
352
  return 0;
296
353
  }
297
354
 
355
+ // A probe turns the flag into a live incident: the claim in `what` is only
356
+ // recordable while the probe command fails. A passing probe means there is
357
+ // nothing to diagnose, so refuse instead of storing a story.
358
+ if (options.probe === true) throw new Error('--probe needs a command');
359
+ const probe = normalizeSpaces(options.probe || '');
360
+ let probeExit = null;
361
+ if (probe) {
362
+ const result = (context.runProbe || runProbe)(probe, context);
363
+ if (result.pass) {
364
+ throw new Error('probe passes right now; an incident needs a command that currently fails. fix the probe or drop --probe');
365
+ }
366
+ probeExit = result.exit_code;
367
+ }
368
+
298
369
  const openedAt = now.toISOString();
299
370
  const event = {
300
371
  kind: 'opened',
@@ -308,11 +379,88 @@ function commandAdd(args, context = {}) {
308
379
  close_condition: normalizeSpaces(options.when || 'the loop is resolved'),
309
380
  source: normalizeSpaces(options.source || 'manual'),
310
381
  };
382
+ if (probe) event.probe = probe;
311
383
  appendEvent(event, context.cwd);
312
- console.log(`opened ${event.id}`);
384
+ if (probe) {
385
+ console.log(`opened ${event.id}, probe failing as required (exit ${probeExit})`);
386
+ } else {
387
+ console.log(`opened ${event.id}`);
388
+ }
313
389
  return 0;
314
390
  }
315
391
 
392
+ // atris close check [id] — run the live probes on open incidents. This is the
393
+ // first move for any session entering an area: if a probe still fails, the
394
+ // recorded theory is still live; do not diagnose fresh. After
395
+ // PROBE_STOP_AFTER_FAILED_CHECKS failing re-checks the verdict is automatic:
396
+ // stop and tell the human.
397
+ function commandCheck(args, context = {}) {
398
+ const { positional, options } = parseArgs(args);
399
+ const id = positional[0] || null;
400
+ const now = context.now ? new Date(context.now) : new Date();
401
+
402
+ let flags = openFlags(context.cwd, { now }).filter((flag) => flag.probe);
403
+ if (id) {
404
+ flags = flags.filter((flag) => flag.id === id);
405
+ if (flags.length === 0) throw new Error(`${id} is not an open incident with a probe`);
406
+ }
407
+ if (flags.length === 0) {
408
+ console.log('no open incidents with probes.');
409
+ return 0;
410
+ }
411
+
412
+ const results = [];
413
+ for (const flag of flags) {
414
+ const run = (context.runProbe || runProbe)(flag.probe, context);
415
+ appendEvent({
416
+ kind: 'probed',
417
+ at: now.toISOString(),
418
+ id: flag.id,
419
+ pass: run.pass,
420
+ exit_code: run.exit_code,
421
+ }, context.cwd);
422
+ results.push({ id: flag.id, what: flag.what, ...run });
423
+ }
424
+
425
+ const after = new Map(
426
+ openFlags(context.cwd, { now }).map((flag) => [flag.id, flag])
427
+ );
428
+
429
+ if (options.json) {
430
+ printJson({
431
+ checked: results.map((result) => {
432
+ const flag = after.get(result.id);
433
+ return {
434
+ ...result,
435
+ probe_status: flag ? flag.probe_status : null,
436
+ failed_checks: flag ? flag.failed_checks : null,
437
+ };
438
+ }),
439
+ });
440
+ return 0;
441
+ }
442
+
443
+ let anyFailing = false;
444
+ for (const result of results) {
445
+ const flag = after.get(result.id);
446
+ if (result.pass) {
447
+ if (flag && flag.probe_holding) {
448
+ console.log(`${result.id}: probe held. close it: atris close done ${result.id}`);
449
+ } else {
450
+ console.log(`${result.id}: probe passed once. re-check in an hour to confirm it holds.`);
451
+ }
452
+ continue;
453
+ }
454
+ anyFailing = true;
455
+ const failedChecks = flag ? flag.failed_checks : 0;
456
+ console.log(`${result.id}: probe still failing (exit ${result.exit_code}). the recorded theory is still live.`);
457
+ if (failedChecks >= PROBE_STOP_AFTER_FAILED_CHECKS) {
458
+ console.log(`${result.id}: ${failedChecks} fix attempts have not moved the probe. stop and tell the human.`);
459
+ }
460
+ }
461
+ return anyFailing ? 1 : 0;
462
+ }
463
+
316
464
  function commandList(args, context = {}) {
317
465
  const { options } = parseArgs(args);
318
466
  const lane = options.lane ? String(options.lane).toLowerCase() : null;
@@ -346,13 +494,25 @@ function commandDone(args, context = {}) {
346
494
  const { positional, options } = parseArgs(args);
347
495
  const id = positional[0];
348
496
  if (!id) throw new Error('id is required');
349
- requireOpenFlag(id, context);
497
+ const flag = requireOpenFlag(id, context);
498
+
499
+ // An incident with a live probe closes on held evidence, not on a claim:
500
+ // the probe must pass twice, an hour apart, so the storm that returns
501
+ // hours later cannot be marked solved in the same breath as the fix.
502
+ if (flag.probe && !flag.probe_holding) {
503
+ if (flag.probe_status === 'failing') {
504
+ throw new Error(`probe still fails; run the fix, then: atris close check ${id}`);
505
+ }
506
+ throw new Error(`probe passed once; confirm it holds with atris close check ${id} at least an hour after the first pass`);
507
+ }
508
+
350
509
  const at = (context.now ? new Date(context.now) : new Date()).toISOString();
510
+ const defaultProof = flag.probe ? `probe held: ${flag.probe}` : '';
351
511
  appendEvent({
352
512
  kind: 'closed',
353
513
  at,
354
514
  id,
355
- proof: normalizeSpaces(options.proof || ''),
515
+ proof: normalizeSpaces(options.proof || defaultProof),
356
516
  }, context.cwd);
357
517
  console.log(`closed ${id}`);
358
518
  return 0;
@@ -1097,6 +1257,7 @@ function run(args = [], context = {}) {
1097
1257
  if (subcommand === 'snooze') return commandSnooze(rest, context);
1098
1258
  if (subcommand === 'sweep') return commandSweep(rest, context);
1099
1259
  if (subcommand === 'scan') return commandScan(rest, context);
1260
+ if (subcommand === 'check') return commandCheck(rest, context);
1100
1261
  console.error(`unknown close command: ${subcommand}`);
1101
1262
  return 2;
1102
1263
  } catch (error) {
@@ -23,6 +23,7 @@ const {
23
23
  } = require('../lib/runner-command');
24
24
  const {
25
25
  resolveEngineForRoleWithPreference,
26
+ engineFailureHealthStatus,
26
27
  setEngineHealth,
27
28
  } = require('../lib/engine-registry');
28
29
  const {
@@ -378,28 +379,6 @@ function resolveMissionTickRunner(mission, root = process.cwd()) {
378
379
  };
379
380
  }
380
381
 
381
- function engineFailureHealthStatus(result) {
382
- if (!result || result.status !== 'errored') return null;
383
- const signalText = [
384
- result.reason,
385
- result.model_unavailable,
386
- result.claude && result.claude.summary,
387
- result.claude && result.claude.receipt_text,
388
- result.claude && result.claude.stderr,
389
- result.rate_limit_info && JSON.stringify(result.rate_limit_info),
390
- ].filter(Boolean).join('\n').toLowerCase();
391
- if (/usage[ _-]?limit|purchase more credits|insufficient credits|credit(?:s)?[ _-]?(?:out|limit)|rate[ _-]?limit/.test(signalText)) {
392
- return 'credit_out';
393
- }
394
- if (/timeout|model-unavailable/.test(signalText)) return 'not_installed';
395
- // Any other errored tick (claude-error, no-ready-engine's sibling failures,
396
- // etc.) is still a real failure signal for the engine that ran it. Falling
397
- // through to null here left the registry showing "ready" for an engine
398
- // that had just hard-failed (e.g. a 401), so auto routing kept sending
399
- // ticks back to it. Mark it "error" instead of silently doing nothing.
400
- return 'error';
401
- }
402
-
403
382
  function recordMissionEngineTickOutcome(engineId, result, root = process.cwd()) {
404
383
  if (!engineId) return null;
405
384
  const status = result && result.status === 'ran' ? 'ready' : engineFailureHealthStatus(result);
@@ -1,8 +1,12 @@
1
1
  const { apiRequestJson } = require('../utils/api');
2
2
  const { ensureValidCredentials } = require('../utils/auth');
3
3
  const { spawnSync } = require('child_process');
4
+ const path = require('path');
4
5
  const https = require('https');
5
6
 
7
+ const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [haiku|atris-fast|gemini|grok|codex|cursor]';
8
+ const YTNOTES_HINT = 'zero credits, local captions + a fast engine';
9
+
6
10
  const DEFAULT_QUERY = [
7
11
  'Create a timestamped YouTube brief for Atris.',
8
12
  'Include: metadata, timestamped outline, core claims with confidence, memorable examples, actionable takeaways, Atris/product implications, and next actions.',
@@ -19,9 +23,11 @@ const ALLOWED_CAPTION_HOST_SUFFIXES = [
19
23
 
20
24
  function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
21
25
  output('');
22
- output(`Usage: ${commandName} process <youtube-url> [options]`);
26
+ output(`Usage: ${commandName} notes <youtube-url> [engine]`);
27
+ output(` ${commandName} process <youtube-url> [options]`);
23
28
  output(` ${commandName} <youtube-url> [options]`);
24
29
  output('');
30
+ output('notes = free local notes, process = 5 credits cloud knowledge');
25
31
  output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
26
32
  output('Falls back to cloud video processing when local captions are unavailable.');
27
33
  output('');
@@ -37,6 +43,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
37
43
  output(' metadata -> timestamped outline -> claims -> examples -> takeaways -> Atris implications -> next actions');
38
44
  output('');
39
45
  output('Examples:');
46
+ output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID`);
40
47
  output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
41
48
  output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
42
49
  output('');
@@ -437,8 +444,31 @@ function formatYoutubeResult(data) {
437
444
  return lines.join('\n');
438
445
  }
439
446
 
447
+ function runYoutubeNotes(args = [], deps = {}) {
448
+ const output = deps.output || ((line = '') => console.error(line));
449
+ const url = args[0];
450
+ const engine = args[1];
451
+ if (!url) {
452
+ output(YTNOTES_USAGE);
453
+ output(YTNOTES_HINT);
454
+ return 2;
455
+ }
456
+
457
+ const script = path.join(__dirname, '..', 'scripts', 'det', 'ytnotes');
458
+ const spawn = deps.spawnSync || spawnSync;
459
+ const childArgs = engine ? [url, engine] : [url];
460
+ const result = spawn(script, childArgs, { stdio: 'inherit' });
461
+ if (result.status == null) return 1;
462
+ return result.status;
463
+ }
464
+
440
465
  async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
441
466
  const output = deps.output || ((line = '') => console.log(line));
467
+ if (argv[0] === 'notes') {
468
+ const code = runYoutubeNotes(argv.slice(1), deps);
469
+ if (!deps.output && !deps.spawnSync) process.exit(code);
470
+ return code;
471
+ }
442
472
  const options = parseYoutubeArgs(argv);
443
473
  if (options.help) {
444
474
  showYoutubeHelp(output, deps.commandName || 'atris youtube');