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.
- package/commands/autoland.js +24 -10
- package/commands/mission.js +9 -1
- package/commands/task.js +49 -14
- package/lib/auto-accept-certified.js +14 -5
- package/lib/context-gatherer.js +2 -0
- package/lib/first-minute.js +11 -5
- package/lib/roster-runs.js +5 -1
- package/lib/roster-suggest.js +1 -1
- package/lib/roster-swap.js +1 -1
- package/lib/router-brain.js +1 -1
- package/lib/voice-gate.js +1 -0
- package/package.json +1 -1
package/commands/autoland.js
CHANGED
|
@@ -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
|
|
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 &&
|
|
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}
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
package/commands/mission.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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) =>
|
|
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 {
|
package/lib/context-gatherer.js
CHANGED
|
@@ -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 {
|
package/lib/first-minute.js
CHANGED
|
@@ -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
|
|
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 "${
|
|
242
|
-
: `atris task ready --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
|
-
|
|
718
|
+
...starter,
|
|
713
719
|
status: starter.status || 'claimed',
|
|
714
720
|
}, person);
|
|
715
721
|
}
|
package/lib/roster-runs.js
CHANGED
|
@@ -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(', ')}`;
|
package/lib/roster-suggest.js
CHANGED
|
@@ -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)
|
package/lib/roster-swap.js
CHANGED
|
@@ -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.
|
package/lib/router-brain.js
CHANGED
|
@@ -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