atris 3.74.21 → 3.74.23
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/bin/atris.js +5 -1
- package/commands/autoland.js +24 -10
- package/commands/improve.js +1 -1
- package/commands/mission.js +29 -5
- package/commands/rsi.js +25 -1
- package/commands/spend.js +78 -0
- package/commands/task.js +29 -11
- package/lib/auto-accept-certified.js +4 -3
- package/lib/claude-spend-guard.js +288 -0
- package/lib/engine-ask.js +5 -2
- package/lib/engine-validate.js +2 -1
- package/lib/known-commands.js +1 -1
- package/lib/member-save-work.js +2 -1
- package/lib/roster-models.js +3 -1
- 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/rsi-loop.js +353 -0
- package/lib/runner-command.js +66 -2
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -669,7 +669,7 @@ function showHelpAll() {
|
|
|
669
669
|
console.log(' land - The landing: what is actually done vs still in the air; --reap backs up + clears overdue');
|
|
670
670
|
console.log(' caretaker - Classify open pull requests on origin (scan only; no fix, comment, or merge)');
|
|
671
671
|
console.log(' drive - One self-driving tick: mission doctor -> auto-fix -> count disengagements');
|
|
672
|
-
console.log(' rsi - Read the Dream-RSI attempt ledger
|
|
672
|
+
console.log(' rsi - Read the Dream-RSI attempt ledger; rsi loop ships notebook fixes with no mission');
|
|
673
673
|
console.log(` autoland - Approve the policy once; ${require('../lib/autoland').certifiedWorkLandsPhrase(process.cwd())}, you keep irreversible calls`);
|
|
674
674
|
console.log(' engine - engine registry, answer validation, dispatch flights, and live progress');
|
|
675
675
|
console.log(' link - What two things could become together: one line, why, a first thing to make, who makes it');
|
|
@@ -2495,6 +2495,10 @@ if (command === 'guide') {
|
|
|
2495
2495
|
Promise.resolve().then(() => require('../commands/balance').balanceCommand(process.argv.slice(3)))
|
|
2496
2496
|
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
2497
2497
|
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2498
|
+
} else if (command === 'spend') {
|
|
2499
|
+
// Spend guard: unattended Claude runs on paid models need a yes (2026-10-28, $900 day).
|
|
2500
|
+
const code = require('../commands/spend').spendCommand(process.argv.slice(3));
|
|
2501
|
+
exitAfterStdoutDrain(typeof code === 'number' ? code : 0);
|
|
2498
2502
|
} else if (command === 'usage') {
|
|
2499
2503
|
Promise.resolve().then(() => require('../commands/usage').usageCommand(process.argv.slice(3)))
|
|
2500
2504
|
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
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/improve.js
CHANGED
|
@@ -453,7 +453,7 @@ function collectImproveVitals(options = {}, deps = {}) {
|
|
|
453
453
|
return fs.existsSync(plistPath);
|
|
454
454
|
})();
|
|
455
455
|
const installed = launchdInstalled || legacyCronInstalled;
|
|
456
|
-
const installNudge = installed ? null : 'the scheduled improve loop is off. turn it on: atris pulse install
|
|
456
|
+
const installNudge = installed ? null : 'the scheduled improve loop is off. turn it on: atris pulse install';
|
|
457
457
|
|
|
458
458
|
const experimentsPath = path.join(root, '.atris', 'state', 'experiments-daily.json');
|
|
459
459
|
const experiments = readJsonFile(experimentsPath, {});
|
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' };
|
|
@@ -9353,7 +9360,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
9353
9360
|
`## Your task`,
|
|
9354
9361
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
9355
9362
|
`- ${memberIdentityLine(mission.owner)} Your member files are preloaded above; open atris/team/${mission.owner}/MEMBER.md only if that copy is marked truncated. Work in that identity, inside its scope and stop rules. Atris writes your member log from your receipt, so end the receipt with one \`learned: <one line>\` line instead of editing the log.`,
|
|
9356
|
-
`-
|
|
9363
|
+
`- Start with the job: inspect the files or state directly relevant to the objective, then do the smallest useful action. Skip workspace boot and start-of-session routines, including brain activation, general Atris or task listings, and reading MAP, persona, or skill files solely for startup. Inspect mission or task state only if the job requires it.`,
|
|
9357
9364
|
`- Pick the smallest concrete action that moves the mission forward.`,
|
|
9358
9365
|
`- Before acting, state your single next move in one sentence.`,
|
|
9359
9366
|
`- Edit / run / research as needed for the lane.`,
|
|
@@ -10075,6 +10082,16 @@ function engineHealthRoot(cwd) {
|
|
|
10075
10082
|
return dir && path.basename(dir) === '.git' ? path.dirname(dir) : cwd;
|
|
10076
10083
|
}
|
|
10077
10084
|
|
|
10085
|
+
function reviewerHealthRoot(engine) {
|
|
10086
|
+
return path.join(require('../lib/state-home').atrisHome(), 'reviewer-health', engine);
|
|
10087
|
+
}
|
|
10088
|
+
|
|
10089
|
+
function reviewerCooling(engine, registry) {
|
|
10090
|
+
const saved = registry.readEngineRegistry(reviewerHealthRoot(engine), { persist: false })
|
|
10091
|
+
.engines.find((entry) => entry.id === engine);
|
|
10092
|
+
return saved?.health?.status === 'cooling' && !registry.engineReadyAt(saved);
|
|
10093
|
+
}
|
|
10094
|
+
|
|
10078
10095
|
// Every spelling and version of one model family counts as the same model,
|
|
10079
10096
|
// with or without the long-context tag. Unknown counts as same.
|
|
10080
10097
|
function reviewerMayBeSameModel(a, b) {
|
|
@@ -10102,12 +10119,12 @@ function engineVerifierLineup(mission, cwd, preferredEngine = '', options = {})
|
|
|
10102
10119
|
const workerEngine = canonicalEngineName(mission?.runner) || 'claude';
|
|
10103
10120
|
const workerModel = resolveMissionTickRunnerModel(mission);
|
|
10104
10121
|
const team = registry.resolveEngineForRoleRanked('validator', root, { job: 'review', quiet: true }).team || [];
|
|
10105
|
-
const backup = team.find((engine) => engine && engine.id !== primary
|
|
10122
|
+
const backup = team.find((engine) => engine && engine.id !== primary && !reviewerCooling(engine.id, registry)
|
|
10106
10123
|
&& !(engine.id === workerEngine && reviewerMayBeSameModel(engine.roster_model, workerModel)));
|
|
10107
10124
|
if (!backup) return lineup;
|
|
10108
10125
|
lineup.push({ engine: backup.id, model: backup.roster_model || '' });
|
|
10109
10126
|
const saved = registry.readEngineRegistry(root, { persist: false }).engines.find((engine) => engine.id === primary);
|
|
10110
|
-
if (saved && !registry.engineReadyAt(saved)) lineup.shift();
|
|
10127
|
+
if ((saved && !registry.engineReadyAt(saved)) || reviewerCooling(primary, registry)) lineup.shift();
|
|
10111
10128
|
} catch {}
|
|
10112
10129
|
return lineup;
|
|
10113
10130
|
}
|
|
@@ -10127,7 +10144,7 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
10127
10144
|
sessionId: crypto.randomUUID(),
|
|
10128
10145
|
cwd,
|
|
10129
10146
|
signal: options.signal,
|
|
10130
|
-
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
10147
|
+
timeoutMs: options.timeoutMs || ENGINE_VERIFY_TIMEOUT_MS,
|
|
10131
10148
|
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1, missionDoneLooksLike(mission, cwd), {
|
|
10132
10149
|
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
10133
10150
|
checkPassed: options.checkPassed || '',
|
|
@@ -10140,11 +10157,16 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
10140
10157
|
restoreRunnerProfile();
|
|
10141
10158
|
}
|
|
10142
10159
|
verdict = engineVerifierResultFromRun(engineResult, engine);
|
|
10160
|
+
if (engineResult?.ok && !engineResult?.timedOut && !engineResult?.aborted) {
|
|
10161
|
+
try { setEngineHealth(engine, 'ready', healthRoot); } catch {}
|
|
10162
|
+
try { setEngineHealth(engine, 'ready', reviewerHealthRoot(engine)); } catch {}
|
|
10163
|
+
}
|
|
10143
10164
|
if (verdict.mode !== 'engine-unavailable' || engineResult?.aborted) return verdict;
|
|
10144
10165
|
// Member lab 10/26: codex hung on every review, and each job waited out
|
|
10145
10166
|
// the full five minutes. Bench it so the next review starts on the backup.
|
|
10146
10167
|
if (engineResult?.timedOut) {
|
|
10147
10168
|
try { setEngineHealth(engine, 'cooling', healthRoot); } catch {}
|
|
10169
|
+
try { setEngineHealth(engine, 'cooling', reviewerHealthRoot(engine)); } catch {}
|
|
10148
10170
|
}
|
|
10149
10171
|
}
|
|
10150
10172
|
return verdict;
|
|
@@ -11103,7 +11125,8 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
11103
11125
|
next_push: result.next_push || undefined,
|
|
11104
11126
|
engine: result.engine_id || runtimeMission.runner || mission.runner || null,
|
|
11105
11127
|
task_type: 'executor',
|
|
11106
|
-
verified_passed:
|
|
11128
|
+
verified_passed: missionTickRunOutcome(result, verifierResult).outcome === 'review unavailable'
|
|
11129
|
+
? undefined : verifierResult && typeof verifierResult.passed === 'boolean' ? verifierResult.passed : undefined,
|
|
11107
11130
|
duration_ms: Math.max(0, (Date.parse(finishedAt) || 0) - (Date.parse(tickStart) || 0)),
|
|
11108
11131
|
}).mission;
|
|
11109
11132
|
if (rotateSessionForContext) {
|
|
@@ -12999,6 +13022,7 @@ module.exports = {
|
|
|
12999
13022
|
receiptLearnedLine,
|
|
13000
13023
|
tickTouchedCheckDefinition,
|
|
13001
13024
|
engineVerifierLineup,
|
|
13025
|
+
runEngineVerifier,
|
|
13002
13026
|
claudeRunnerMcpArgs,
|
|
13003
13027
|
crystallizeMemberSkillLesson,
|
|
13004
13028
|
listSiblingWorkspaceMissionHints,
|
package/commands/rsi.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// outcomes, the live policy id, and the last dream receipt. Read-only: it
|
|
7
7
|
// never touches record.py, never writes state, and exits 0 even when the
|
|
8
8
|
// workspace has no ledger yet.
|
|
9
|
+
//
|
|
10
|
+
// `atris rsi loop` ships notebook fixes with no mission queued (lib/rsi-loop.js).
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const rsi = require('../lib/rsi-record');
|
|
@@ -114,8 +116,29 @@ Usage:
|
|
|
114
116
|
atris rsi same as status
|
|
115
117
|
atris rsi status trees + attempts by lane, last night's outcomes, policy, last dream
|
|
116
118
|
atris rsi status --json machine-readable status
|
|
119
|
+
atris rsi loop [--minutes M] [--json]
|
|
120
|
+
find notebook fixes with no mission queued, verify, commit and
|
|
121
|
+
push them on one reusable isolated branch (default budget 5 minutes)
|
|
122
|
+
|
|
123
|
+
status is read-only. loop checks atris/ pages for line refs that point at
|
|
124
|
+
moved code and for wording the slop rules can repair, commits each check
|
|
125
|
+
that verifies, and pushes the branch. It removes the local branch and checkout
|
|
126
|
+
only once the remote tip matches; the remote branch is reused on later runs.
|
|
127
|
+
The budget is checked between steps and limits network operations.
|
|
128
|
+
The main checkout is never edited.
|
|
129
|
+
|
|
130
|
+
State lives in .atris/state/rsi/ (or $ATRIS_RSI_STATE); loop reports go to loops.jsonl there.`);
|
|
131
|
+
}
|
|
117
132
|
|
|
118
|
-
|
|
133
|
+
async function runLoopCommand(args) {
|
|
134
|
+
const rsiLoop = require('../lib/rsi-loop');
|
|
135
|
+
const i = args.indexOf('--minutes');
|
|
136
|
+
const raw = i >= 0 ? String(args[i + 1] || '') : '';
|
|
137
|
+
const minutes = i < 0 ? undefined : (/^\d+(\.\d+)?$/.test(raw) ? Number(raw) : NaN);
|
|
138
|
+
const result = await rsiLoop.runLoop({ minutes, workspace: process.cwd() });
|
|
139
|
+
console.log(args.includes('--json') ? JSON.stringify(result) : rsiLoop.formatLoopReport(result));
|
|
140
|
+
if (result.refused) return 2;
|
|
141
|
+
return result.ok ? 0 : 1;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
function run(argv = []) {
|
|
@@ -125,6 +148,7 @@ function run(argv = []) {
|
|
|
125
148
|
return 0;
|
|
126
149
|
}
|
|
127
150
|
const sub = args[0] && !args[0].startsWith('-') ? args[0] : 'status';
|
|
151
|
+
if (sub === 'loop') return runLoopCommand(args.slice(1));
|
|
128
152
|
if (sub !== 'status') {
|
|
129
153
|
console.log(`unknown rsi subcommand: ${sub}`);
|
|
130
154
|
showHelp();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const guard = require('../lib/claude-spend-guard');
|
|
5
|
+
|
|
6
|
+
function showSpendHelp() {
|
|
7
|
+
console.log('usage: atris spend guard [install|uninstall|status]');
|
|
8
|
+
console.log(' atris spend log [--limit N]');
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log('The guard stops unattended Claude runs on paid models (anything but Haiku)');
|
|
11
|
+
console.log(`unless ${guard.APPROVAL_ENV}=<model> names the model for that run.`);
|
|
12
|
+
console.log('It also stops unattended runs that carry a pay-per-use Anthropic key.');
|
|
13
|
+
console.log('Interactive Claude sessions are never blocked.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function printStatus(status) {
|
|
17
|
+
if (!status.readable) {
|
|
18
|
+
console.log(`guard: unknown, could not read ${status.settings}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(status.installed ? 'guard: on' : 'guard: off');
|
|
22
|
+
console.log(`settings: ${status.settings}`);
|
|
23
|
+
console.log(`hook: ${status.script}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function guardCommand(action = 'status') {
|
|
27
|
+
if (action === 'install') {
|
|
28
|
+
const status = guard.installGuard();
|
|
29
|
+
printStatus(status);
|
|
30
|
+
if (status.installed) {
|
|
31
|
+
console.log('Unattended Claude runs on paid models now need a yes on this computer.');
|
|
32
|
+
}
|
|
33
|
+
return status.installed ? 0 : 1;
|
|
34
|
+
}
|
|
35
|
+
if (action === 'uninstall') {
|
|
36
|
+
printStatus(guard.uninstallGuard());
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (action === 'status') {
|
|
40
|
+
const status = guard.guardStatus();
|
|
41
|
+
printStatus(status);
|
|
42
|
+
return status.installed ? 0 : 1;
|
|
43
|
+
}
|
|
44
|
+
showSpendHelp();
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function logCommand(args) {
|
|
49
|
+
const i = args.indexOf('--limit');
|
|
50
|
+
const limit = i >= 0 ? Math.max(1, Number(args[i + 1]) || 20) : 20;
|
|
51
|
+
const file = guard.ledgerPath();
|
|
52
|
+
if (!fs.existsSync(file)) {
|
|
53
|
+
console.log('no guard decisions recorded on this computer.');
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const rows = fs.readFileSync(file, 'utf8').trim().split('\n').slice(-limit);
|
|
57
|
+
for (const line of rows) {
|
|
58
|
+
try {
|
|
59
|
+
const r = JSON.parse(line);
|
|
60
|
+
console.log(`${r.ts} ${r.decision} ${r.model} ${r.job || ''} ${r.cwd || ''}`.trimEnd());
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function spendCommand(args = []) {
|
|
67
|
+
const [sub, ...rest] = args;
|
|
68
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
69
|
+
showSpendHelp();
|
|
70
|
+
return sub ? 0 : 1;
|
|
71
|
+
}
|
|
72
|
+
if (sub === 'guard') return guardCommand(rest[0] || 'status');
|
|
73
|
+
if (sub === 'log') return logCommand(rest);
|
|
74
|
+
showSpendHelp();
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { spendCommand };
|
package/commands/task.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
parseVerifyCommand,
|
|
32
32
|
runVerifyCommand,
|
|
33
33
|
DENIED_TAGS,
|
|
34
|
+
protectedLaneOf,
|
|
34
35
|
} = require('../lib/auto-accept-certified');
|
|
35
36
|
const { evaluateAcceptVerify, nonFalsifyingIssue } = require('../lib/accept-verify-gate');
|
|
36
37
|
const { checkedCodeSha, runVerifyAtJobCode } = require('../lib/job-code-verify');
|
|
@@ -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,7 +10713,7 @@ 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' };
|
|
@@ -10819,14 +10834,17 @@ function cmdSweep(args) {
|
|
|
10819
10834
|
for (const item of pendingReview) {
|
|
10820
10835
|
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
10821
10836
|
const task = fullProjection.tasks[0] || null;
|
|
10822
|
-
const
|
|
10837
|
+
const includeProtected = hasFlag(args, '--include-protected');
|
|
10838
|
+
const evaluation = evaluateSweepAutoAccept(task, root, includeProtected);
|
|
10823
10839
|
if (!evaluation.eligible) {
|
|
10824
10840
|
results.push({ ...evaluation, action: 'skipped', task_id: task?.id || item.id || null });
|
|
10825
10841
|
continue;
|
|
10826
10842
|
}
|
|
10827
10843
|
const accepted = acceptReviewTask(taskDb, db, task.id, {
|
|
10828
10844
|
actor,
|
|
10829
|
-
proof:
|
|
10845
|
+
proof: includeProtected && (protectedLaneOf(task) || autoAcceptSweepLane(task))
|
|
10846
|
+
? `${evaluation.proof}\nprotected lane accepted by owner policy (${protectedLaneOf(task) || autoAcceptSweepLane(task)})`
|
|
10847
|
+
: evaluation.proof,
|
|
10830
10848
|
reward: parsedReward.value,
|
|
10831
10849
|
lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
|
|
10832
10850
|
nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
|
|
@@ -1177,6 +1177,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1177
1177
|
strictVerify = true,
|
|
1178
1178
|
minPasses = AGENT_CERTIFICATION_REVIEW_PASSES,
|
|
1179
1179
|
acceptAll = false,
|
|
1180
|
+
includeProtected = false,
|
|
1180
1181
|
executeVerify = true,
|
|
1181
1182
|
verifyCache = null,
|
|
1182
1183
|
} = options;
|
|
@@ -1200,7 +1201,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1200
1201
|
// strip, so a tag variant never slips money/deploy work past the human.
|
|
1201
1202
|
const tag = String(task.tag || '').trim().toLowerCase();
|
|
1202
1203
|
const deniedTag = deniedTagOf(task);
|
|
1203
|
-
if (deniedTag) return { eligible: false, ref, reason: `denied_tag_${deniedTag}` };
|
|
1204
|
+
if (deniedTag && !includeProtected) return { eligible: false, ref, reason: `denied_tag_${deniedTag}` };
|
|
1204
1205
|
|
|
1205
1206
|
// A protected lane declared in prose is not a protection, it is a note.
|
|
1206
1207
|
// WEB-410 said "PROTECTED LANE: auth/session minting ... never self-land"
|
|
@@ -1208,7 +1209,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1208
1209
|
// safety through a tag field that authors leave empty fails open, silently.
|
|
1209
1210
|
// So read the declaration where authors actually write it.
|
|
1210
1211
|
const declared = declaredProtectedLane(task);
|
|
1211
|
-
if (declared) {
|
|
1212
|
+
if (declared && !includeProtected) {
|
|
1212
1213
|
return {
|
|
1213
1214
|
eligible: false,
|
|
1214
1215
|
ref,
|
|
@@ -1236,7 +1237,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1236
1237
|
// same outage as failing open, just quieter. Instead: when there is no
|
|
1237
1238
|
// tag to route on, read the task text for the denied lanes themselves.
|
|
1238
1239
|
// WEB-406 (Stripe checkout + paywall) landed untagged this way.
|
|
1239
|
-
if (!tag) {
|
|
1240
|
+
if (!tag && !includeProtected) {
|
|
1240
1241
|
const sniffed = sniffedProtectedLane(task);
|
|
1241
1242
|
if (sniffed) {
|
|
1242
1243
|
return {
|