atris 3.82.3 → 3.82.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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ Mem0, Zep and Letta remember the users of a chat app. Atris remembers the projec
28
28
  npx -y atris aeo scan yoursite.com --vs competitor.com
29
29
  ```
30
30
 
31
- You get a score out of 100, each gap with its fix, a side-by-side score against a competitor, and a draft `llms.txt` you can publish. To see what ChatGPT actually says when buyers ask about your business, run the free check at https://atris.ai/aeo/check.
31
+ You get a score out of 100, each gap with its fix, a side-by-side score against a competitor, and a draft `llms.txt` you can publish. To see what ChatGPT actually says when buyers ask about your business, run the free check at https://atris.ai/aeo/check. Guide to both checks and the tools that run them: [docs/answer-engine-checks.md](docs/answer-engine-checks.md).
32
32
 
33
33
  ## Quick Start
34
34
 
package/atris/atris.md CHANGED
@@ -149,6 +149,12 @@ The Mission anchors, goals point, a mission run commits, tasks prove, accepts la
149
149
 
150
150
  ## task source of truth
151
151
 
152
+ For the next agent step, use `atris next --agent <functional-member> --json`.
153
+ Known worker shells choose this view automatically; `--human` shows the
154
+ person's view without changing acceptance permission. An unchecked task
155
+ requires a goal, exit, first move and runnable check through `atris task plan`.
156
+ The recommendation itself never claims, executes, accepts or awards XP.
157
+
152
158
  Use `atris task` as the source of truth for active work. It stores durable local
153
159
  SQLite state plus append-only task events, and refreshes
154
160
  `.atris/state/tasks.projection.json` for desktop/web UIs. `atris/TODO.md` is a
package/bin/atris.js CHANGED
@@ -633,6 +633,8 @@ function showHelpAll() {
633
633
  console.log(' verify - Validate work is done (tests, MAP.md, changes)');
634
634
  console.log(' slop - Deterministic slop detector: frontend/prose tells, dead code (slop dead --exports)');
635
635
  console.log(' sentinel - Security audit: engines hunt for holes, a different model verifies each one');
636
+ console.log(' free checks: sentinel agents, sentinel watch, sentinel guard install');
637
+ console.log(' re-check old findings against today\'s code: sentinel recheck');
636
638
  console.log(' voice - Scan binary voice tells or judge reply shape and plainness');
637
639
  console.log(' recover - Explain stopped missions; --apply performs safe recovery only');
638
640
  console.log(' task - Local agent task plane (atomic claims, TODO import)');
package/commands/aeo.js CHANGED
@@ -217,10 +217,23 @@ async function aeoScan(args) {
217
217
  console.log('\n Gaps:');
218
218
  for (const g of gaps) console.log(` ✗ ${g.title || g.key}: ${g.fix || ''}`);
219
219
  }
220
+ // What an agent actually gets from live requests: plain text on request,
221
+ // crawler access, words without scripts, structured data, sitemap.
222
+ const access = data.agent_access;
223
+ if (access && Array.isArray(access.checks)) {
224
+ console.log(`\n Agent access: ${access.score}/${access.max_score ?? 100}`);
225
+ for (const c of access.checks) {
226
+ console.log(` ${c.passed ? '✓' : '✗'} ${c.title}${c.passed ? '' : `: ${c.fix || c.detail || ''}`}`);
227
+ }
228
+ }
220
229
  const comp = data.competitors || [];
221
230
  if (comp.length) {
222
- console.log('\n Head-to-head:');
223
- for (const c of comp) console.log(` ${pad(String(c.score ?? '?'), 4)} ${c.url || c.name}`);
231
+ const withAccess = comp.some((c) => c.agent_access_score != null);
232
+ console.log(withAccess ? '\n Head-to-head (score, agent access):' : '\n Head-to-head:');
233
+ for (const c of comp) {
234
+ const extra = withAccess ? `${pad(String(c.agent_access_score ?? '?'), 4)} ` : '';
235
+ console.log(` ${pad(String(c.score ?? '?'), 4)} ${extra}${c.url || c.name}`);
236
+ }
224
237
  }
225
238
  if (data.draft_llms_txt) {
226
239
  console.log('\n Draft llms.txt included, save it with:');
package/commands/next.js CHANGED
@@ -19,7 +19,7 @@ const {
19
19
  } = require('../lib/next-moves');
20
20
 
21
21
  function showHelp(log = console.log) {
22
- log('Usage: atris next [--json]');
22
+ log('Usage: atris next [--agent <member>|--human] [--json]');
23
23
  return 0;
24
24
  }
25
25
 
@@ -179,12 +179,17 @@ function executeCard(card, root, log = console.log) {
179
179
 
180
180
  function actionToken(args = []) {
181
181
  const list = Array.isArray(args) ? args : [];
182
- const token = list.find((arg) => {
182
+ for (let i = 0; i < list.length; i += 1) {
183
+ const arg = list[i];
183
184
  const text = String(arg || '').trim();
184
- if (!text || text.startsWith('-')) return false;
185
- return true;
186
- });
187
- return String(token || '').trim().toLowerCase();
185
+ if (['--agent', '--as', '--task', '--goal-id', '--goal_id', '--tag', '--status', '--review-state', '--review_state'].includes(text)) {
186
+ if (list[i + 1] && !String(list[i + 1]).startsWith('-')) i += 1;
187
+ continue;
188
+ }
189
+ if (!text || text.startsWith('-')) continue;
190
+ return text.toLowerCase();
191
+ }
192
+ return '';
188
193
  }
189
194
 
190
195
  function nextCommand(args = [], root = process.cwd(), { log = console.log } = {}) {
@@ -193,6 +198,11 @@ function nextCommand(args = [], root = process.cwd(), { log = console.log } = {}
193
198
 
194
199
  const asJson = wantsJson(list);
195
200
  const action = actionToken(list);
201
+ if (!['skip', 'no', 'yes'].includes(action)) {
202
+ const { buildAgentNext, printAgentNext } = require('../lib/agent-next');
203
+ const agent = buildAgentNext({ root, args: list });
204
+ if (agent) return printAgentNext(agent, { asJson, log });
205
+ }
196
206
 
197
207
  if (!action || action === 'json') return speakNext(root, { asJson, log });
198
208
 
package/commands/now.js CHANGED
@@ -612,7 +612,7 @@ function nowJsonPayload(root = process.cwd()) {
612
612
  function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
613
613
  const help = args.includes('--help') || args.includes('-h') || args[0] === 'help';
614
614
  if (help) {
615
- console.log('Usage: atris now [--init|--refresh|--all|--path] [--json]');
615
+ console.log('Usage: atris now [--init|--refresh|--all|--path] [--agent <member>|--human] [--json]');
616
616
  console.log('');
617
617
  console.log('Show the current operating truth for this workspace.');
618
618
  console.log('');
@@ -632,6 +632,11 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
632
632
  const asJson = args.includes('--json');
633
633
 
634
634
  try {
635
+ if (!init && !refresh && !all && !pathOnly) {
636
+ const { buildAgentNext, printAgentNext } = require('../lib/agent-next');
637
+ const agent = buildAgentNext({ root, args });
638
+ if (agent) return printAgentNext(agent, { asJson, alias: 'next' });
639
+ }
635
640
  if (asJson && !init && !refresh && !all && !pathOnly) {
636
641
  const payload = nowJsonPayload(root);
637
642
  printNowJson(payload);
@@ -457,13 +457,15 @@ function renderAuditLanding({ dir, meta, records }) {
457
457
  if (hunters < 1) L.push(' this budget is too small to send a hunter; try --budget 8 or more');
458
458
  else L.push(needed <= hunters ? ' one run can read every area' : ` about ${Math.ceil(needed / hunters)} runs to read every area`);
459
459
  } else {
460
- L.push(` ${confirmed.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
460
+ const waiting = require('../lib/security-audit').waitingOf(meta);
461
+ L.push(` ${confirmed.length + waiting.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
461
462
  L.push(` read ${cov.fresh} areas this run, ${cov.carried} carried unchanged, ${cov.deferred || 0} left for the next run`);
462
463
  L.push(` found by ${meta.engines.hunter}, checked by ${meta.engines.verifier}${meta.engines.hunter === meta.engines.verifier ? ' (same engine)' : ''} · ${meta.invocations.used} engine calls of ${meta.budget}${meta.credits_used ? ` · ${meta.credits_used} credits used` : ''}`);
463
464
  for (const r of confirmed.slice(0, 6)) {
464
465
  const sink = r.trace[r.trace.length - 1];
465
466
  L.push(` ${r.severity.overall.padEnd(8)} ${r.title} (${sink.file}:${sink.line})`);
466
467
  }
468
+ for (const w of waiting.slice(0, 6)) L.push(` ${w.severity.padEnd(8)} ${w.title} (last seen at ${w.location}, still open: ${w.why})`);
467
469
  if (meta.run_status === 'incomplete') L.push(` incomplete: ${String(meta.incomplete_reason).replace(/_/g, ' ')}`);
468
470
  }
469
471
  L.push(` report: ${tildePath(path.join(dir, 'REPORT.md'))}`, '');
@@ -478,11 +480,28 @@ function askYesNo(question, { input = process.stdin, output = process.stdout } =
478
480
  });
479
481
  }
480
482
 
481
- async function uploadAfterRun(dir, { token, quiet, cloud }) {
483
+ async function uploadAfterRun(dir, { token, quiet, cloud, root = null, env = process.env }) {
482
484
  const res = await cloud.uploadRun(dir, { token });
483
485
  const say = quiet ? (m) => console.error(m) : (m) => console.log(m);
484
486
  if (res.ok) say(' saved the summary to your atris account (no source code was sent)');
485
487
  else say(` warning: could not save the summary to your atris account: ${res.reason}. the review itself is done`);
488
+ if (res.ok && res.body) await sendFixedEvents(dir, res.body, { token, root, env, say });
489
+ return res;
490
+ }
491
+
492
+ // Fixed findings also go to the events feed. The overview counts them as
493
+ // fixed today. A failure here never undoes the saved review.
494
+ async function sendFixedEvents(dir, body, { token, root, env, say }) {
495
+ if (!body.findings.some((f) => f.status === 'fixed')) return null;
496
+ let meta = null;
497
+ try { meta = JSON.parse(fs.readFileSync(path.join(dir, 'run-metadata.json'), 'utf8')); } catch {}
498
+ const target = root || (meta && meta.target) || process.cwd();
499
+ let res;
500
+ try {
501
+ res = await require('../lib/sentinel-events').reportReviewFixes(target, body, { token, env, foundAt: meta && meta.prior_finished_at });
502
+ } catch (e) { res = { ok: false, reason: e.message }; }
503
+ if (res.ok && res.fixed) say(` marked ${res.fixed} finding${res.fixed === 1 ? '' : 's'} fixed on your dashboard`);
504
+ else if (!res.ok) say(` warning: could not mark the fixed findings on your dashboard: ${res.reason}. run atris sentinel upload to try again`);
486
505
  return res;
487
506
  }
488
507
 
@@ -502,6 +521,7 @@ async function auditUploadCommand(argv, deps) {
502
521
  const res = await cloud.uploadRun(dir, { token });
503
522
  if (!res.ok) { console.error(` could not save ${path.basename(dir)}: ${res.reason}`); return 1; }
504
523
  console.log(` saved ${path.basename(dir)} to your atris account (no source code was sent)`);
524
+ if (res.body) await sendFixedEvents(dir, res.body, { token, root: null, env: deps.env || process.env, say: (m) => console.log(m) });
505
525
  return 0;
506
526
  }
507
527
 
@@ -590,6 +610,7 @@ async function auditScheduleCommand(argv, deps) {
590
610
  if (!installed.ok) { cloud.removeApproval(target, deps.homeDir); console.error(` could not install the timer: ${installed.error}`); return 1; }
591
611
  console.log(` scheduled reviews are on for ${tildePath(target)}, once a ${every}`);
592
612
  console.log(' each run uses the quick profile and saves its summary to your atris account');
613
+ console.log(' each run also checks your agent setup and dependencies for free');
593
614
  if (approval.engine === 'local') console.log(' runs use only codex and claude on this machine. they never switch to atris models');
594
615
  else console.log(` runs use atris models and stop at ${approval.max_credits} credits`);
595
616
  console.log(` log: ${tildePath(plan.log_path)}`);
@@ -597,6 +618,119 @@ async function auditScheduleCommand(argv, deps) {
597
618
  return 0;
598
619
  }
599
620
 
621
+ // The quote-and-confirm step every run on atris models goes through: price
622
+ // the estimate and set the cap. Then a yes, --yes or a scheduled approval.
623
+ function spendConfirmer({ cloud, token, spend, capOverride, opts, deps, tell }) {
624
+ return async (estimate) => {
625
+ const quote = await cloud.quoteEstimate(estimate, { token });
626
+ spend.max = capOverride || quote.credits;
627
+ spend.limits = quote.limits;
628
+ tell(`\n ${cloud.describeQuote(quote)}\n`);
629
+ if (capOverride) tell(` this scheduled run may spend at most ${capOverride} credits, as approved`);
630
+ if (opts.yes) return true;
631
+ const input = deps.input || process.stdin;
632
+ if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return false; }
633
+ const yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: opts.json ? process.stderr : process.stdout });
634
+ if (!yes) tell(' nothing was spent');
635
+ return yes;
636
+ };
637
+ }
638
+
639
+ function parseRecheckArgs(argv) {
640
+ const opts = { json: false, verifier: null, engine: null, yes: false, upload: true, timeoutMs: null, budget: null, target: null };
641
+ for (let i = 0; i < argv.length; i++) {
642
+ const arg = argv[i];
643
+ const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, null];
644
+ const value = () => {
645
+ if (inline !== null) return inline;
646
+ if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
647
+ return argv[++i];
648
+ };
649
+ if (flag === '--json') opts.json = true;
650
+ else if (flag === '--verifier') opts.verifier = value();
651
+ else if (flag === '--engine') opts.engine = value();
652
+ else if (flag === '--yes' || flag === '-y') opts.yes = true;
653
+ else if (flag === '--no-upload') opts.upload = false;
654
+ else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
655
+ else if (flag === '--budget') opts.budget = Number(value());
656
+ else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
657
+ else if (opts.target) throw new Error('give one folder to re-check');
658
+ else opts.target = arg;
659
+ }
660
+ if (opts.engine !== null && !['atris', 'local'].includes(opts.engine)) throw new Error('--engine must be atris or local');
661
+ if (opts.budget !== null && (!Number.isInteger(opts.budget) || opts.budget < 1 || opts.budget > 500)) throw new Error('--budget must be a whole number from 1 to 500');
662
+ if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
663
+ return opts;
664
+ }
665
+
666
+ const OUTCOME_WORDS = { fixed: 'fixed', open: 'still open', moved: 'still open, moved', unsure: 'unsure, kept open', held: 'kept open' };
667
+
668
+ function renderRecheckLanding(result) {
669
+ const { meta, records, dir } = result;
670
+ const a = meta.audit;
671
+ const byFp = new Map(records.map((r) => [r.fingerprint, r]));
672
+ const L = ['', ` ◉ sentinel re-check · ${meta.repo}`, ''];
673
+ L.push(` re-checked ${a.checked} open finding${a.checked === 1 ? '' : 's'} from ${path.basename(meta.prior_run)}`);
674
+ L.push(` ${a.fixed} fixed · ${a.open} still open · ${a.unchanged} unchanged with no call`);
675
+ for (const n of a.notes) {
676
+ const r = byFp.get(n.fingerprint);
677
+ const held = (a.held || []).find((h) => h.fingerprint === n.fingerprint);
678
+ const title = r ? r.title : held ? held.title : n.fingerprint;
679
+ const sev = r && r.severity ? r.severity.overall : held ? held.severity : '';
680
+ L.push(` ${(OUTCOME_WORDS[n.outcome] || n.outcome).padEnd(18)} ${sev ? `${sev} ` : ''}${title}`);
681
+ L.push(` ${''.padEnd(18)} ${n.why}`);
682
+ }
683
+ L.push(` checked by ${meta.engines.verifier} · ${meta.invocations.used} engine call${meta.invocations.used === 1 ? '' : 's'}${meta.credits_used ? ` · ${meta.credits_used} credits used` : ''}`);
684
+ if (meta.run_status === 'incomplete') L.push(' some findings got no answer. they stay open and wait for the next run');
685
+ L.push(` report: ${tildePath(path.join(dir, 'REPORT.md'))}`, '');
686
+ return L.join('\n');
687
+ }
688
+
689
+ // atris sentinel recheck [path]: only the auditor, on the last review's open
690
+ // findings. Same engine rules, same price question and cap on atris models,
691
+ // same plain-sentence upload.
692
+ async function auditRecheckCommand(argv, deps = {}) {
693
+ const audit = require('../lib/security-audit');
694
+ const cloud = deps.cloud || require('../lib/sentinel-cloud');
695
+ let opts;
696
+ try { opts = parseRecheckArgs(argv); } catch (e) { console.error(`sentinel recheck: ${e.message}`); return 2; }
697
+ const root = path.resolve(opts.target || deps.root || process.cwd());
698
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) { console.error(`sentinel recheck: ${root} is not a folder`); return 2; }
699
+ const token = deps.token !== undefined ? deps.token : cloud.authToken();
700
+ let engines;
701
+ try { engines = cloud.resolveEngines({ engine: opts.engine, verifier: opts.verifier, token, env: deps.env || process.env }); } catch (e) { console.error(`sentinel recheck: ${e.message}`); return 2; }
702
+ const verifier = engines.verifier || undefined;
703
+ const hosted = audit.isHostedEngine(verifier);
704
+ if (hosted && !token) { console.error('sentinel recheck: re-checks on atris models need a login; run atris login first'); return 2; }
705
+ const tell = opts.json ? (m) => console.error(m) : (m) => console.log(m);
706
+ if (hosted && engines.chosen === 'no_local_engine') tell(' no codex or claude found on this machine. this re-check runs on atris models');
707
+ const spend = hosted ? cloud.spendGuard({ quoteCall: deps.quoteCall }) : null;
708
+ let runJobs = deps.runJobs;
709
+ if (!runJobs && hosted) runJobs = cloud.atrisJobRunner({ token, spend });
710
+ const confirmSpend = hosted ? spendConfirmer({ cloud, token, spend, capOverride: null, opts, deps, tell }) : null;
711
+ let result;
712
+ try {
713
+ result = await audit.runRecheck({ root, verifier, runJobs, confirmSpend, budget: opts.budget ?? undefined, timeoutMs: opts.timeoutMs || undefined, log: opts.json ? () => {} : (m) => console.log(` · ${m}`) });
714
+ } catch (e) {
715
+ console.error(`sentinel recheck: ${e.message}`);
716
+ return 2;
717
+ }
718
+ if (result.nothing) {
719
+ if (opts.json) console.log(JSON.stringify({ ok: true, nothing: result.nothing }, null, 2));
720
+ else console.log(`\n ${result.nothing}. nothing to do\n`);
721
+ return 0;
722
+ }
723
+ if (result.declined) return 1;
724
+ const fail = audit.shouldFailRun(result.dir, result.records);
725
+ if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
726
+ else console.log(renderRecheckLanding(result));
727
+ if (spend && spend.blocked) tell(` stopped at the approved limit of ${spend.max} credits. ${spend.blocked} model call${spend.blocked === 1 ? '' : 's'} did not run. those findings stay open`);
728
+ if (opts.upload && token) {
729
+ try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud, root, env: deps.env || process.env }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the re-check itself is done`); }
730
+ }
731
+ return fail ? 1 : 0;
732
+ }
733
+
600
734
  async function auditCommand(argv, deps = {}) {
601
735
  if (argv[0] === 'help' || argv.includes('-h') || argv.includes('--help')) return printAuditHelp();
602
736
  const audit = require('../lib/security-audit');
@@ -608,7 +742,13 @@ async function auditCommand(argv, deps = {}) {
608
742
  console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
609
743
  return 0;
610
744
  }
745
+ if (['agents', 'guard', 'watch', 'flush'].includes(argv[0])) {
746
+ const powers = require('./sentinel-powers');
747
+ const run = { agents: powers.agentsCommand, guard: powers.guardCommand, watch: powers.watchCommand, flush: powers.flushCommand }[argv[0]];
748
+ return run(argv.slice(1), deps);
749
+ }
611
750
  if (argv[0] === 'upload') return auditUploadCommand(argv.slice(1), deps);
751
+ if (argv[0] === 'recheck') return auditRecheckCommand(argv.slice(1), deps);
612
752
  if (argv[0] === 'schedule') return auditScheduleCommand(argv.slice(1), deps);
613
753
  let opts;
614
754
  try { opts = parseAuditArgs(argv); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
@@ -618,6 +758,11 @@ async function auditCommand(argv, deps = {}) {
618
758
  if (opts.scheduled) {
619
759
  const approval = cloud.readApproval(root, deps.homeDir);
620
760
  if (!approval) { console.error('security-review audit: this folder has no approved schedule. nothing ran. run atris sentinel schedule again'); return 2; }
761
+ // The free checks run first, so a review that stops at its credit limit
762
+ // still leaves today's agent and dependency results.
763
+ try {
764
+ await require('./sentinel-powers').runScheduledFreeChecks(root, { ...deps, upload: opts.upload, log: opts.json ? (m) => console.error(m) : (m) => console.log(m) });
765
+ } catch (e) { console.error(` warning: the free checks did not finish: ${e.message}`); }
621
766
  opts.hunter = null;
622
767
  opts.verifier = null;
623
768
  opts.engine = approval.engine;
@@ -636,19 +781,7 @@ async function auditCommand(argv, deps = {}) {
636
781
  let runJobs = deps.runJobs;
637
782
  const spend = hosted ? cloud.spendGuard({ quoteCall: deps.quoteCall }) : null;
638
783
  if (!runJobs && hosted) runJobs = cloud.atrisJobRunner({ token, spend });
639
- const confirmSpend = async (estimate) => {
640
- const quote = await cloud.quoteEstimate(estimate, { token });
641
- spend.max = capOverride || quote.credits;
642
- spend.limits = quote.limits;
643
- tell(`\n ${cloud.describeQuote(quote)}\n`);
644
- if (capOverride) tell(` this scheduled run may spend at most ${capOverride} credits, as approved`);
645
- if (opts.yes) return true;
646
- const input = deps.input || process.stdin;
647
- if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return false; }
648
- const yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: opts.json ? process.stderr : process.stdout });
649
- if (!yes) tell(' nothing was spent');
650
- return yes;
651
- };
784
+ const confirmSpend = spendConfirmer({ cloud, token, spend, capOverride, opts, deps, tell });
652
785
  let result;
653
786
  try {
654
787
  result = await audit.runAudit({
@@ -661,12 +794,12 @@ async function auditCommand(argv, deps = {}) {
661
794
  return 2;
662
795
  }
663
796
  if (result.declined) return 1;
664
- const fail = audit.shouldFailAudit(result.records);
797
+ const fail = !opts.planOnly && audit.shouldFailRun(result.dir, result.records);
665
798
  if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
666
799
  else console.log(renderAuditLanding(result));
667
800
  if (spend && spend.blocked) tell(` stopped at the approved limit of ${spend.max} credits. ${spend.blocked} model call${spend.blocked === 1 ? '' : 's'} did not run and wait for the next run`);
668
801
  if (opts.upload && token && !opts.planOnly) {
669
- try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the review itself is done`); }
802
+ try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud, root, env: deps.env || process.env }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the review itself is done`); }
670
803
  }
671
804
  return fail ? 1 : 0;
672
805
  }
@@ -703,9 +836,30 @@ function printAuditHelp() {
703
836
  atris sentinel --no-upload keep this run on this machine only
704
837
  atris sentinel upload [run-folder] save a past run (default: the last one)
705
838
 
839
+ re-checking old findings (every review does this first):
840
+
841
+ atris sentinel recheck [path] only re-check the last review's open findings
842
+ atris sentinel recheck --engine atris re-check on atris models, price shown first
843
+
844
+ a finding whose code did not change stays open without a call. a changed
845
+ one goes to the verifier, never the engine that found it. it is marked
846
+ fixed only with a yes and the line that now closes it. unsure stays open.
847
+
706
848
  only the summary is saved: plain sentences, file:line locations and counts.
707
849
  source code and code lines never leave in an upload.
708
850
 
851
+ free checks, no model and no credits:
852
+
853
+ atris sentinel agents [path] check agent instructions, settings, hooks, mcp and skills
854
+ atris sentinel agents --home also check ~/.claude and ~/.codex
855
+ atris sentinel watch [path] known attacks in your dependencies (osv database)
856
+ atris sentinel watch --fix update affected packages on a new branch, never pushed
857
+ atris sentinel guard install [--global] stop risky agent commands before they run
858
+ atris sentinel guard test see which sample commands the guard stops
859
+
860
+ results go to your atris dashboard when you are logged in. add --no-upload
861
+ to keep them on this machine. secret values and file contents never leave.
862
+
709
863
  reviews on a timer:
710
864
 
711
865
  atris sentinel schedule --every day [path] quick review once a day, saved to your account
@@ -714,6 +868,7 @@ function printAuditHelp() {
714
868
  timed runs on atris models, at most 20 credits each
715
869
  atris sentinel schedule --uninstall [path] stop the timer
716
870
 
871
+ every timed run also runs the free agents and watch checks.
717
872
  the engine is picked when the timer goes on. on atris models you see the
718
873
  price and say yes then. a timed run never switches to atris models later.
719
874