atris 3.74.20 → 3.74.22

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.
@@ -449,7 +449,7 @@ function wishSweepSummaryLine(summary, error = null) {
449
449
  return line;
450
450
  }
451
451
 
452
- function evaluateQueue(root, { strictVerify, acceptAll }) {
452
+ function evaluateQueue(root, { strictVerify, acceptAll, includeProtected }) {
453
453
  return readProjection(root)
454
454
  .filter((task) => task && task.status === 'review')
455
455
  .filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
@@ -458,7 +458,7 @@ function evaluateQueue(root, { strictVerify, acceptAll }) {
458
458
  .map((task) => {
459
459
  const evaluation = evaluateAutoAccept(
460
460
  { ...task, workspace_root: root },
461
- { strictVerify, acceptAll, executeVerify: false },
461
+ { strictVerify, acceptAll, includeProtected, executeVerify: false },
462
462
  );
463
463
  return {
464
464
  ...evaluation,
@@ -472,6 +472,7 @@ function protectedReviewWaiting(root, policy = {}, waiting = []) {
472
472
  return evaluateQueue(root, {
473
473
  strictVerify: policy.strict_verify !== false,
474
474
  acceptAll: Boolean(policy.accept_all),
475
+ includeProtected: Boolean(policy.accept_protected),
475
476
  })
476
477
  .filter((row) => /^denied_tag_/.test(String(row.reason || '')))
477
478
  .filter((row) => !alreadyWaiting.has(row.ref));
@@ -487,7 +488,8 @@ function showStatus(root, args, deps = {}) {
487
488
  const enabled = Boolean(policy && policy.enabled);
488
489
  const strictVerify = policy ? policy.strict_verify !== false : true;
489
490
  const acceptAll = Boolean(policy && policy.accept_all);
490
- const results = evaluateQueue(root, { strictVerify, acceptAll })
491
+ const includeProtected = Boolean(policy && policy.accept_protected);
492
+ const results = evaluateQueue(root, { strictVerify, acceptAll, includeProtected })
491
493
  .filter((r) => r.reason !== 'not_in_review');
492
494
  const readyForRecheck = results.filter((r) => r.action === 'would_accept');
493
495
  const wouldLand = readyForRecheck.filter((r) => r.verification_pending !== true);
@@ -525,7 +527,8 @@ function showStatus(root, args, deps = {}) {
525
527
  : `on (flipped by ${policyOwner}${policy.enabled_at ? ` on ${String(policy.enabled_at).slice(0, 10)}` : ''})`)
526
528
  : 'off - everything waits for you';
527
529
  console.log(` policy: ${policyText}`);
528
- if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
530
+ if (enabled && includeProtected) console.log(' bar: protected work (money, deploys, security, customer, outward) also lands itself; sends and payments still need you.');
531
+ else if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
529
532
  console.log(` heartbeat: ${autoland.heartbeatStatusText(root, policy)}`);
530
533
  if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
531
534
  const reapTrouble = autoland.readState(root).last_reap_error;
@@ -634,6 +637,7 @@ function turnOn(root, args) {
634
637
  alarm_hours: Number.isFinite(alarmHours) && alarmHours > 0 ? alarmHours : autoland.DEFAULT_ALARM_HOURS,
635
638
  strict_verify: !args.includes('--no-strict-verify'),
636
639
  accept_all: acceptAll,
640
+ accept_protected: args.includes('--include-protected'),
637
641
  daily_experiment: previous.daily_experiment !== false,
638
642
  });
639
643
  const cronOk = autoland.installCron(root);
@@ -644,9 +648,11 @@ function turnOn(root, args) {
644
648
  });
645
649
  console.log('');
646
650
  console.log('autoland is on.');
647
- if (acceptAll) console.log(` everything in review now lands itself, accepted as ${owner}; only the protected lanes wait.`);
651
+ if (acceptAll && policy.accept_protected) console.log(` everything in review now lands itself, accepted as ${owner}.`);
652
+ else if (acceptAll) console.log(` everything in review now lands itself, accepted as ${owner}; only the protected lanes wait.`);
648
653
  else console.log(` certified, verified, reversible work now lands itself, accepted as ${owner}.`);
649
- console.log(' protected lanes (money, deploys, security, customer, outward) still wait for you.');
654
+ if (policy.accept_protected) console.log(' protected work (money, deploys, security, customer, outward) also lands itself; sends and payments still need you.');
655
+ else console.log(' protected lanes (money, deploys, security, customer, outward) still wait for you.');
650
656
  console.log(` heartbeat: ${cronOk ? 'installed, runs hourly' : 'could not install cron; run atris autoland tick yourself'}`);
651
657
  if (policy.imessage_to) {
652
658
  console.log(` daily message to ${policy.imessage_to} at ${policy.digest_hour}:00; anything waiting on you past ${policy.alarm_hours}h pings you.`);
@@ -1020,6 +1026,14 @@ function digestSentToday(state, now = new Date()) {
1020
1026
  return state.last_digest_date === utcDay(now) && state.last_digest_date !== utcDay(yesterday);
1021
1027
  }
1022
1028
 
1029
+ function acceptCommandArgs(policy) {
1030
+ const args = ['task', 'auto-accept-certified', '--json', '--certify-first'];
1031
+ if (policy.accept_protected) args.push('--include-protected');
1032
+ if (policy.accept_all) args.push('--all');
1033
+ else if (policy.strict_verify === false) args.push('--no-strict-verify');
1034
+ return args;
1035
+ }
1036
+
1023
1037
  async function runTickBody(root, { json, policy, receipt, engineValidationDeps = {} }) {
1024
1038
 
1025
1039
  try {
@@ -1044,9 +1058,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
1044
1058
  // Refresh origin once, before anything is accepted: the landing proof reads
1045
1059
  // origin's target, and a stale ref would call landed work unlanded.
1046
1060
  refreshLandingRefs(root);
1047
- const cliArgs =['task', 'auto-accept-certified', '--json', '--certify-first'];
1048
- if (policy.accept_all) cliArgs.push('--all');
1049
- else if (policy.strict_verify === false) cliArgs.push('--no-strict-verify');
1061
+ const cliArgs = acceptCommandArgs(policy);
1050
1062
  const pendingReviewRows = readProjection(root)
1051
1063
  .filter((task) => task && task.status === 'review')
1052
1064
  .filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
@@ -1442,7 +1454,7 @@ function showHelp() {
1442
1454
  console.log('');
1443
1455
  console.log('finished work that passed its checks and two independent reviews lands');
1444
1456
  console.log('automatically with a receipt. money, deploys, security, customer, and');
1445
- console.log('outward-facing work always waits for you.');
1457
+ console.log('outward-facing work waits for you unless you include protected work.');
1446
1458
  console.log('');
1447
1459
  console.log('recommended agent flight checks:');
1448
1460
  console.log(' fast suite: npm run test:fast');
@@ -1456,6 +1468,7 @@ function showHelp() {
1456
1468
  console.log(' atris autoland on --all lower the bar: everything lands except');
1457
1469
  console.log(' the protected lanes; a failing recorded');
1458
1470
  console.log(' check still blocks');
1471
+ console.log(' atris autoland on --include-protected include protected work in the policy');
1459
1472
  console.log(' atris autoland off back to approving every item');
1460
1473
  console.log(' atris autoland digest [--send] the daily message, now');
1461
1474
  console.log(' atris autoland tick [--json] one heartbeat (what the hourly cron runs)');
@@ -1501,6 +1514,7 @@ module.exports = {
1501
1514
  hasAgentJargon,
1502
1515
  runEngineAnswerValidation,
1503
1516
  runTickBody,
1517
+ acceptCommandArgs,
1504
1518
  runSshAgentSweep,
1505
1519
  sshAgentSweepNote,
1506
1520
  showStatus,
@@ -520,6 +520,13 @@ function recordMissionEngineTickOutcome(engineId, result, root = process.cwd())
520
520
  // a closed credit window is credit out. Never throws into the tick.
521
521
  function missionTickRunOutcome(result, verifierResult, maxSeconds) {
522
522
  if (result && result.status === 'ran') {
523
+ const review = verifierResult?.referee;
524
+ if (verifierResult?.passed === false && verifierResult.status === 0 && !verifierResult.timed_out
525
+ && review?.mode === 'engine-unavailable' && review.reason !== 'code changed during review') {
526
+ return { outcome: 'review unavailable', detail: review.timed_out
527
+ ? 'the build check passed, but the second review timed out'
528
+ : 'the build check passed, but the second review was unavailable' };
529
+ }
523
530
  return verifierResult && verifierResult.passed === false
524
531
  ? { outcome: 'failed', detail: 'the verifier failed' }
525
532
  : { outcome: 'landed' };
@@ -11103,7 +11110,8 @@ async function executeMissionRunTicksPhase(context) {
11103
11110
  next_push: result.next_push || undefined,
11104
11111
  engine: result.engine_id || runtimeMission.runner || mission.runner || null,
11105
11112
  task_type: 'executor',
11106
- verified_passed: verifierResult && typeof verifierResult.passed === 'boolean' ? verifierResult.passed : undefined,
11113
+ verified_passed: missionTickRunOutcome(result, verifierResult).outcome === 'review unavailable'
11114
+ ? undefined : verifierResult && typeof verifierResult.passed === 'boolean' ? verifierResult.passed : undefined,
11107
11115
  duration_ms: Math.max(0, (Date.parse(finishedAt) || 0) - (Date.parse(tickStart) || 0)),
11108
11116
  }).mission;
11109
11117
  if (rotateSessionForContext) {
package/commands/task.js CHANGED
@@ -31,8 +31,9 @@ const {
31
31
  parseVerifyCommand,
32
32
  runVerifyCommand,
33
33
  DENIED_TAGS,
34
+ protectedLaneOf,
34
35
  } = require('../lib/auto-accept-certified');
35
- const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
36
+ const { evaluateAcceptVerify, nonFalsifyingIssue } = require('../lib/accept-verify-gate');
36
37
  const { checkedCodeSha, runVerifyAtJobCode } = require('../lib/job-code-verify');
37
38
  const { proveTaskLanding } = require('../lib/landing-proof');
38
39
  const {
@@ -10089,7 +10090,7 @@ function certifyVerifyCandidate(task) {
10089
10090
  // The evidence extractor can keep trailing prose ("git diff --check. Evidence
10090
10091
  // inspected: ..."); try the full clause, then the first sentence of it.
10091
10092
  for (const candidate of [raw, raw.split(/\.(?:\s|$)/)[0]].map((c) => String(c || '').trim())) {
10092
- if (candidate && isAutoCertifyVerifyCommandAllowed(candidate) && !verifyIsOnlyFileChecks(candidate)) return candidate;
10093
+ if (candidate && !nonFalsifyingIssue(candidate) && isAutoCertifyVerifyCommandAllowed(candidate) && !verifyIsOnlyFileChecks(candidate)) return candidate;
10093
10094
  }
10094
10095
  }
10095
10096
  return null;
@@ -10247,6 +10248,7 @@ function cmdCertifyVerified(args, options = {}) {
10247
10248
  const asJson = wantsJson(args);
10248
10249
  const silent = options.silent === true;
10249
10250
  const verifyCache = options.verifyCache || null;
10251
+ const includeProtected = options.includeProtected === true;
10250
10252
  const actor = String(flag(args, '--as') || 'autoland-verifier');
10251
10253
  const limitRaw = flag(args, '--limit');
10252
10254
  const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 6) : 6;
@@ -10284,7 +10286,7 @@ function cmdCertifyVerified(args, options = {}) {
10284
10286
  continue;
10285
10287
  }
10286
10288
  const tag = String(task.tag || '').toLowerCase();
10287
- if (DENIED_TAGS.has(tag)) {
10289
+ if (!includeProtected && DENIED_TAGS.has(tag)) {
10288
10290
  results.push({ ref, action: 'skipped', reason: `denied_tag_${tag}` });
10289
10291
  continue;
10290
10292
  }
@@ -10302,7 +10304,7 @@ function cmdCertifyVerified(args, options = {}) {
10302
10304
  // second time per row before the real run. executeVerify: false as well,
10303
10305
  // an untrusted trust tier re-enables the strict block internally even
10304
10306
  // with strictVerify off, which is how this probe spawned a verify per row.
10305
- const evaluation = evaluateAutoAccept(task, { strictVerify: false, executeVerify: false });
10307
+ const evaluation = evaluateAutoAccept(task, { strictVerify: false, includeProtected, executeVerify: false });
10306
10308
  if (evaluation.eligible) {
10307
10309
  results.push({ ref, action: 'skipped', reason: 'already_landable' });
10308
10310
  continue;
@@ -10415,6 +10417,7 @@ function cmdLanding(args) {
10415
10417
  function cmdAutoAcceptCertified(args) {
10416
10418
  const dryRun = hasFlag(args, '--dry-run');
10417
10419
  const acceptAll = hasFlag(args, '--all');
10420
+ const includeProtected = hasFlag(args, '--include-protected');
10418
10421
  const certifyFirst = hasFlag(args, '--certify-first');
10419
10422
  const strictVerify = !hasFlag(args, '--no-strict-verify') && !acceptAll;
10420
10423
  const actorFlag = flag(args, '--as');
@@ -10479,7 +10482,7 @@ function cmdAutoAcceptCertified(args) {
10479
10482
  // never persisted: a later heartbeat must prove the checkout again.
10480
10483
  const verifyCache = new Map();
10481
10484
  const certification = certifyFirst && !dryRun
10482
- ? cmdCertifyVerified([], { verifyCache, silent: true })
10485
+ ? cmdCertifyVerified([], { verifyCache, silent: true, includeProtected })
10483
10486
  : null;
10484
10487
 
10485
10488
  const taskDb = getTaskDb();
@@ -10514,7 +10517,7 @@ function cmdAutoAcceptCertified(args) {
10514
10517
  results.push({ ...proofBoundary, action: 'skipped' });
10515
10518
  continue;
10516
10519
  }
10517
- const evaluation = evaluateAutoAccept(task, { strictVerify, acceptAll, verifyCache });
10520
+ const evaluation = evaluateAutoAccept(task, { strictVerify, acceptAll, includeProtected, verifyCache });
10518
10521
  if (!evaluation.eligible) {
10519
10522
  results.push({ ...evaluation, action: 'skipped' });
10520
10523
  continue;
@@ -10547,7 +10550,9 @@ function cmdAutoAcceptCertified(args) {
10547
10550
  }
10548
10551
  const accepted = acceptReviewTask(taskDb, db, task.id, {
10549
10552
  actor,
10550
- proof: evaluation.proof,
10553
+ proof: includeProtected && protectedLaneOf(task)
10554
+ ? `${evaluation.proof}\nprotected lane accepted by owner policy (${protectedLaneOf(task)})`
10555
+ : evaluation.proof,
10551
10556
  reward: parsedReward.value,
10552
10557
  lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
10553
10558
  nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
@@ -10632,7 +10637,17 @@ function autoAcceptSweepNormalizedTokens(value) {
10632
10637
  return [normalized, ...normalized.split(/[^a-z0-9-]+/).filter(Boolean)];
10633
10638
  }
10634
10639
 
10635
- function autoAcceptSweepDeniedReason(task) {
10640
+ function autoAcceptSweepLane(task) {
10641
+ for (const value of autoAcceptSweepLabelValues(task)) {
10642
+ for (const token of autoAcceptSweepNormalizedTokens(value)) {
10643
+ const lane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) => token === denied || token.replace(/s$/, '') === denied);
10644
+ if (lane) return lane;
10645
+ }
10646
+ }
10647
+ return null;
10648
+ }
10649
+
10650
+ function autoAcceptSweepDeniedReason(task, includeProtected = false) {
10636
10651
  for (const value of autoAcceptSweepLabelValues(task)) {
10637
10652
  const tokens = autoAcceptSweepNormalizedTokens(value);
10638
10653
  for (const token of tokens) {
@@ -10642,7 +10657,7 @@ function autoAcceptSweepDeniedReason(task) {
10642
10657
  const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
10643
10658
  token === denied || token.replace(/s$/, '') === denied
10644
10659
  );
10645
- if (protectedLane) return 'protected_lane';
10660
+ if (protectedLane && !includeProtected) return 'protected_lane';
10646
10661
  }
10647
10662
  }
10648
10663
  return null;
@@ -10689,7 +10704,7 @@ function autoAcceptSweepHappened(task) {
10689
10704
  );
10690
10705
  }
10691
10706
 
10692
- function evaluateSweepAutoAccept(task, root) {
10707
+ function evaluateSweepAutoAccept(task, root, includeProtected = false) {
10693
10708
  const ref = taskRef(task);
10694
10709
  if (!task) return { eligible: false, ref, reason: 'task_not_found' };
10695
10710
  if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
@@ -10698,10 +10713,14 @@ function evaluateSweepAutoAccept(task, root) {
10698
10713
  const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
10699
10714
  if (approval !== 'pending') return { eligible: false, ref, reason: `approval_${approval}` };
10700
10715
  if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
10701
- const denied = autoAcceptSweepDeniedReason(task);
10716
+ const denied = autoAcceptSweepDeniedReason(task, includeProtected);
10702
10717
  if (denied) return { eligible: false, ref, reason: denied };
10703
10718
  const proof = autoAcceptSweepLatestProof(task);
10704
10719
  if (!proof) return { eligible: false, ref, reason: 'no_proof' };
10720
+ const storedVerify = typeof metadata.verify === 'string' ? metadata.verify.trim() : '';
10721
+ if (storedVerify && nonFalsifyingIssue(storedVerify)) {
10722
+ return { eligible: false, ref, reason: 'non_falsifying_verify', verify: storedVerify };
10723
+ }
10705
10724
  const policyGate = candidatePolicyGate(task, { executeDetectors: true });
10706
10725
  if (!policyGate.ok) return { ...policyGate, ref };
10707
10726
 
@@ -10712,7 +10731,6 @@ function evaluateSweepAutoAccept(task, root) {
10712
10731
  // executed) in the proof, but the older receipt-path check below could not
10713
10732
  // find file evidence for it once `ready --verify` stopped writing a
10714
10733
  // receipt file and started embedding the executed result into proof text.
10715
- const storedVerify = typeof metadata.verify === 'string' ? metadata.verify.trim() : '';
10716
10734
  if (storedVerify) {
10717
10735
  const result = runVerifyCommand(storedVerify, root);
10718
10736
  if (!result.ok) return { eligible: false, ref, reason: result.reason, verify: storedVerify };
@@ -10770,6 +10788,12 @@ function evaluateSweepAutoAccept(task, root) {
10770
10788
  }
10771
10789
  return { eligible: false, ref, reason: result.reason, verify: derived };
10772
10790
  }
10791
+ // Certification filters these out, but the sweep should explain why a
10792
+ // legacy proof with only whitespace evidence cannot approve the work.
10793
+ const proofCommands = taskReviewEvidenceCommands(proof);
10794
+ if (proofCommands.length && proofCommands.every(command => nonFalsifyingIssue(command))) {
10795
+ return { eligible: false, ref, reason: 'non_falsifying_verify' };
10796
+ }
10773
10797
  }
10774
10798
 
10775
10799
  return { eligible: false, ref, reason: verifier.reason, evidence: verifier.evidence };
@@ -10783,6 +10807,14 @@ function cmdSweep(args) {
10783
10807
  'atris task sweep currently requires --auto-accept for the explicit verified accept policy',
10784
10808
  );
10785
10809
  }
10810
+ // Sweep has no dry-run implementation: every successful invocation can
10811
+ // write accepts, even when an unrecognized --dry-run flag is supplied.
10812
+ if (agentProofOnlyMode()) {
10813
+ failAgentProofOnly(
10814
+ 'atris task sweep',
10815
+ 'agents cannot accept tasks or award xp. leave proof in review for a human to accept or revise.',
10816
+ );
10817
+ }
10786
10818
  const actor = String(flag(args, '--as') || 'orb-autoaccept');
10787
10819
  const parsedReward = parseAcceptReward(flag(args, '--reward'));
10788
10820
  if (!parsedReward.ok) {
@@ -10802,14 +10834,17 @@ function cmdSweep(args) {
10802
10834
  for (const item of pendingReview) {
10803
10835
  const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
10804
10836
  const task = fullProjection.tasks[0] || null;
10805
- const evaluation = evaluateSweepAutoAccept(task, root);
10837
+ const includeProtected = hasFlag(args, '--include-protected');
10838
+ const evaluation = evaluateSweepAutoAccept(task, root, includeProtected);
10806
10839
  if (!evaluation.eligible) {
10807
10840
  results.push({ ...evaluation, action: 'skipped', task_id: task?.id || item.id || null });
10808
10841
  continue;
10809
10842
  }
10810
10843
  const accepted = acceptReviewTask(taskDb, db, task.id, {
10811
10844
  actor,
10812
- proof: evaluation.proof,
10845
+ proof: includeProtected && (protectedLaneOf(task) || autoAcceptSweepLane(task))
10846
+ ? `${evaluation.proof}\nprotected lane accepted by owner policy (${protectedLaneOf(task) || autoAcceptSweepLane(task)})`
10847
+ : evaluation.proof,
10813
10848
  reward: parsedReward.value,
10814
10849
  lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
10815
10850
  nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
@@ -10,6 +10,7 @@ const { computeTrustTier, taskTrustSubject } = require('./trust-tiers');
10
10
  const { rewriteWorktreeVerify, missingOnMain } = require('./verify-worktree-rewrite');
11
11
  const { policyLessonsForFiles, readPolicyLessons } = require('./policy-lessons');
12
12
  const { skillEvalGate } = require('./skill-eval-gate');
13
+ const escapeRegExp = require('./escape-regexp');
13
14
 
14
15
  const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
15
16
 
@@ -1074,9 +1075,10 @@ function declaredProtectedLane(task) {
1074
1075
  // work and would hold the whole queue.
1075
1076
  const PROTECTED_LANE_TERMS = [
1076
1077
  ['billing', ['stripe', 'checkout url', 'checkout_url', 'invoice', 'paywall', 'subscription', 'wallet', 'credit card', 'pricing tier']],
1078
+ ['money', ['money', 'refund', 'reimburs', 'expense', 'payment', 'cash', 'bank', 'venmo', 'zelle', 'tax']],
1077
1079
  ['security', ['session minting', 'mints a', 'service token', 'api key', 'credential', 'secret key', 'csrf', 'access token']],
1078
1080
  ['deploy', ['render deploy', 'production deploy', 'deploy to prod', 'migration apply']],
1079
- ['customer', ['send email to', 'customer email', 'outbound email', 'notify users']],
1081
+ ['customer', ['send email to', 'customer email', 'outbound email', 'notify users', 'imessage', 'text message', 'send a text', 'reply to', 'outreach', 'cold email', 'send to', 'dm ']],
1080
1082
  ];
1081
1083
 
1082
1084
  function sniffedProtectedLane(task) {
@@ -1086,7 +1088,7 @@ function sniffedProtectedLane(task) {
1086
1088
  .toLowerCase();
1087
1089
  if (!haystack.trim()) return null;
1088
1090
  for (const [lane, terms] of PROTECTED_LANE_TERMS) {
1089
- const term = terms.find((t) => haystack.includes(t));
1091
+ const term = terms.find((t) => new RegExp(`\\b${escapeRegExp(t)}`).test(haystack));
1090
1092
  if (term) return { lane, term };
1091
1093
  }
1092
1094
  return null;
@@ -1175,6 +1177,7 @@ function evaluateAutoAccept(task, options = {}) {
1175
1177
  strictVerify = true,
1176
1178
  minPasses = AGENT_CERTIFICATION_REVIEW_PASSES,
1177
1179
  acceptAll = false,
1180
+ includeProtected = false,
1178
1181
  executeVerify = true,
1179
1182
  verifyCache = null,
1180
1183
  } = options;
@@ -1198,7 +1201,7 @@ function evaluateAutoAccept(task, options = {}) {
1198
1201
  // strip, so a tag variant never slips money/deploy work past the human.
1199
1202
  const tag = String(task.tag || '').trim().toLowerCase();
1200
1203
  const deniedTag = deniedTagOf(task);
1201
- if (deniedTag) return { eligible: false, ref, reason: `denied_tag_${deniedTag}` };
1204
+ if (deniedTag && !includeProtected) return { eligible: false, ref, reason: `denied_tag_${deniedTag}` };
1202
1205
 
1203
1206
  // A protected lane declared in prose is not a protection, it is a note.
1204
1207
  // WEB-410 said "PROTECTED LANE: auth/session minting ... never self-land"
@@ -1206,7 +1209,7 @@ function evaluateAutoAccept(task, options = {}) {
1206
1209
  // safety through a tag field that authors leave empty fails open, silently.
1207
1210
  // So read the declaration where authors actually write it.
1208
1211
  const declared = declaredProtectedLane(task);
1209
- if (declared) {
1212
+ if (declared && !includeProtected) {
1210
1213
  return {
1211
1214
  eligible: false,
1212
1215
  ref,
@@ -1216,6 +1219,12 @@ function evaluateAutoAccept(task, options = {}) {
1216
1219
  };
1217
1220
  }
1218
1221
 
1222
+ // Load lazily: the human accept gate reuses this module's parser and runner.
1223
+ const verifyCommand = resolveTaskVerify(task).verify;
1224
+ if (verifyCommand && require('./accept-verify-gate').nonFalsifyingIssue(verifyCommand)) {
1225
+ return { eligible: false, ref, reason: 'non_falsifying_verify', verify: verifyCommand };
1226
+ }
1227
+
1219
1228
  // accept-all: the protected lanes above are the only human gate. No
1220
1229
  // certification, pass-count, reviewer, or proof-quality bar, but work
1221
1230
  // is never marked done against evidence it isn't: a proof naming an
@@ -1228,7 +1237,7 @@ function evaluateAutoAccept(task, options = {}) {
1228
1237
  // same outage as failing open, just quieter. Instead: when there is no
1229
1238
  // tag to route on, read the task text for the denied lanes themselves.
1230
1239
  // WEB-406 (Stripe checkout + paywall) landed untagged this way.
1231
- if (!tag) {
1240
+ if (!tag && !includeProtected) {
1232
1241
  const sniffed = sniffedProtectedLane(task);
1233
1242
  if (sniffed) {
1234
1243
  return {
@@ -155,6 +155,8 @@ function createStarterTask(root, answer) {
155
155
  title,
156
156
  status: task && task.status || 'claimed',
157
157
  claimed_by: (task && task.claimed_by) || who,
158
+ workspace_root: workspaceRoot,
159
+ metadata: task && task.metadata,
158
160
  };
159
161
  } catch (error) {
160
162
  return {
@@ -6,10 +6,12 @@ const path = require('path');
6
6
  const { stateHome } = require('./state-home');
7
7
  const { spawnSync } = require('child_process');
8
8
  const { isGenericScratchRoot } = require('./scratch-root');
9
+ const { resolveDefaultVerifier } = require('./default-verifier');
10
+ const { nonFalsifyingIssue } = require('./accept-verify-gate');
9
11
 
10
12
  const FIRST_USE_COMMAND = 'atris "help me choose the first useful step for this project"';
11
13
  const FIRST_TALK_ASK = 'what do you want here';
12
- const CLAIMED_KEEP_WORKING_VERIFY = 'git diff --check';
14
+ const ADD_TEST_COMMAND = 'atris "add a test for the claimed task"';
13
15
  const LATER_NOTE_FILE = '.atris-later';
14
16
  const BARE_ATRIS_FLAGS = new Set(['--yes', '-y', '--json', '--verbose']);
15
17
  const NOT_A_PERSON = new Set([
@@ -237,13 +239,17 @@ function loadLatestRecap(root = process.cwd()) {
237
239
 
238
240
  function claimedKeepWorkingCommand(task) {
239
241
  const ref = taskRef(task);
242
+ const recorded = task && task.metadata && task.metadata.verify;
243
+ const available = recorded || (task && task.workspace_root && resolveDefaultVerifier(task.workspace_root));
244
+ if (!available || nonFalsifyingIssue(available)) return ADD_TEST_COMMAND;
240
245
  return ref
241
- ? `atris task ready ${ref} --verify "${CLAIMED_KEEP_WORKING_VERIFY}"`
242
- : `atris task ready --verify "${CLAIMED_KEEP_WORKING_VERIFY}"`;
246
+ ? `atris task ready ${ref} --verify "${available}"`
247
+ : `atris task ready --verify "${available}"`;
243
248
  }
244
249
 
245
250
  function isClaimedKeepWorkingCommand(command) {
246
- return /^atris task ready\b/.test(String(command || ''));
251
+ return /^atris task ready\b/.test(String(command || ''))
252
+ || command === ADD_TEST_COMMAND;
247
253
  }
248
254
 
249
255
  function taskCommand(task, person) {
@@ -709,7 +715,7 @@ function firstTalkNext({ starter = null, person = '', folder = 'this folder' } =
709
715
  const room = folder || 'this folder';
710
716
  if (starter && starter.display_id) {
711
717
  return taskCommand({
712
- display_id: starter.display_id,
718
+ ...starter,
713
719
  status: starter.status || 'claimed',
714
720
  }, person);
715
721
  }
@@ -21,7 +21,7 @@ const RUNS_FILE = path.join('.atris', 'state', 'roster_runs.jsonl');
21
21
  const ROTATED_NAME = /^roster_runs\.\d.*\.jsonl$/;
22
22
  const ROTATED_KEEP = 3;
23
23
  const ROTATED_READ = 2;
24
- const OUTCOMES = Object.freeze(['landed', 'failed', 'stalled', 'credit out']);
24
+ const OUTCOMES = Object.freeze(['landed', 'failed', 'stalled', 'credit out', 'review unavailable']);
25
25
  const DEFAULT_DAYS = 7;
26
26
  const TAIL_BYTES = 512 * 1024;
27
27
  // Past this size the log is renamed aside before the append, never trimmed:
@@ -131,6 +131,7 @@ function runReason(run) {
131
131
  if (given) return given.slice(0, 240);
132
132
  if (run.outcome === 'stalled') return 'it did not finish in time';
133
133
  if (run.outcome === 'credit out') return 'the engine hit its usage limit';
134
+ if (run.outcome === 'review unavailable') return 'the second review was unavailable';
134
135
  return 'it failed and the tool gave no reason';
135
136
  }
136
137
 
@@ -340,12 +341,15 @@ function summarizeRuns(runs, { days = DEFAULT_DAYS } = {}) {
340
341
  handed_over: list.filter((run) => run.handed_over_to).length,
341
342
  median_seconds: median(list.map((run) => Number(run.seconds)).filter((value) => value >= 0)),
342
343
  };
344
+ const reviewUnavailable = count('review unavailable');
345
+ if (reviewUnavailable) summary.review_unavailable = reviewUnavailable;
343
346
  const tokens = list.map((run) => run.tokens).filter((value) => Number.isFinite(value));
344
347
  if (tokens.length) summary.tokens = tokens.reduce((sum, value) => sum + value, 0);
345
348
  const parts = [`${summary.runs} run${summary.runs === 1 ? '' : 's'}`];
346
349
  if (summary.landed) parts.push(`${summary.landed} landed`);
347
350
  if (summary.stalled) parts.push(`${summary.stalled} stalled`);
348
351
  if (summary.failed) parts.push(`${summary.failed} failed`);
352
+ if (summary.review_unavailable) parts.push(`${summary.review_unavailable} review unavailable`);
349
353
  if (summary.credit_out) parts.push(`${summary.credit_out} credit out`);
350
354
  if (summary.median_seconds !== null) parts.push(`median ${durationText(summary.median_seconds)}`);
351
355
  summary.text = `last ${days} day${days === 1 ? '' : 's'}: ${parts.join(', ')}`;
@@ -26,7 +26,7 @@ const BOOT_TAIL_BYTES = 64 * 1024;
26
26
  // engine health already benches it, so credit out runs are left out.
27
27
  function judgedRuns(runs) {
28
28
  return runs
29
- .filter((run) => run.outcome !== 'credit out')
29
+ .filter((run) => run.outcome !== 'credit out' && run.outcome !== 'review unavailable')
30
30
  .map((run, index) => ({ run, index }))
31
31
  .sort((a, b) => (Date.parse(b.run.at) - Date.parse(a.run.at)) || (b.index - a.index))
32
32
  .slice(0, SUGGEST_WINDOW_RUNS)
@@ -261,7 +261,7 @@ function swapMember(root, memberName, engineWords, modelWords, options = {}) {
261
261
  // model this machine offers that can do the member's work, grouped by tool.
262
262
  // A model's last 7 days in the owner's words: the phone's swap card shows it
263
263
  // under the model, so it says finished, never the team's own word for it.
264
- const recordWords = seen => `${seen.landed} of ${seen.runs} finished this week`;
264
+ const recordWords = seen => `${seen.landed} of ${seen.runs - (seen.review_unavailable || 0)} finished this week${seen.review_unavailable ? `, ${seen.review_unavailable} awaiting review` : ''}`;
265
265
 
266
266
  // The current model is on top; models with a record in the last 7 days come
267
267
  // next, most landed first; the rest keep --available's order.
@@ -219,7 +219,7 @@ function readRosterRunObservations(root, now) {
219
219
  if (!fs.existsSync(file) && !rotatedRunsPaths(root).length) return [];
220
220
  const source = path.relative(root, file);
221
221
  return readRosterRuns(root, { days: ROSTER_RUN_DAYS, ...(now !== undefined ? { now } : {}) }).flatMap((run) => {
222
- if (run.outcome === 'credit out') return [];
222
+ if (run.outcome === 'credit out' || run.outcome === 'review unavailable') return [];
223
223
  const words = String(run.job || '').trim().toLowerCase().split(/\s+/);
224
224
  const role = ROSTER_JOB_ROLES[words[words.length - 1]];
225
225
  const atMs = Date.parse(run.at);
package/lib/voice-gate.js CHANGED
@@ -250,6 +250,7 @@ const LANDING_REASON_SENTENCES = {
250
250
  receipt_verifier_failed: 'a saved receipt failed its re-check, fix what it names and re-certify',
251
251
  slop_gate: 'the touched prose breaks a deterministic taste rule, fix the named lines and re-certify',
252
252
  strict_verify_missing: 'no recorded check command to re-run, add one and re-certify',
253
+ non_falsifying_verify: 'its recorded check cannot prove the work is correct, add a real check and re-certify',
253
254
  untagged_protected_lane_text: 'the description reads like protected-lane work without the tag, tag it or decide it yourself',
254
255
  verification_pending: 'the recorded check has not been re-run yet, the next hourly pass runs it',
255
256
  verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.74.20",
3
+ "version": "3.74.22",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "mcpName": "io.github.atrislabs/atris",
6
6
  "main": "bin/atris.js",