atris 3.69.0 → 3.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/atris/skills/README.md +1 -0
- package/atris/skills/campaigns/SKILL.md +81 -0
- package/atris/skills/meta/SKILL.md +52 -8
- package/ax +226 -42
- package/bin/atris.js +26 -6
- package/commands/autoland.js +176 -7
- package/commands/autopilot.js +71 -11
- package/commands/campaign.js +759 -0
- package/commands/compile.js +19 -8
- package/commands/doctor.js +58 -0
- package/commands/dream.js +18 -3
- package/commands/engine.js +147 -5
- package/commands/improve.js +119 -9
- package/commands/init.js +18 -1
- package/commands/interview.js +15 -3
- package/commands/land.js +22 -6
- package/commands/link.js +1 -0
- package/commands/member.js +433 -99
- package/commands/mission.js +332 -41
- package/commands/report.js +42 -8
- package/commands/run.js +9 -2
- package/commands/scout.js +9 -1
- package/commands/task.js +132 -16
- package/commands/team.js +212 -4
- package/commands/unknowns.js +15 -1
- package/commands/verify.js +46 -0
- package/commands/worktree.js +148 -18
- package/commands/write.js +12 -1
- package/commands/youtube.js +17 -2
- package/lib/accept-verify-gate.js +1 -0
- package/lib/auto-accept-certified.js +1 -0
- package/lib/autoland.js +13 -4
- package/lib/campaign-cadence.js +131 -0
- package/lib/campaign-core.js +330 -0
- package/lib/campaign-holdout.js +65 -0
- package/lib/campaign-loop.js +242 -0
- package/lib/campaign-missions.js +237 -0
- package/lib/campaign-new.js +251 -0
- package/lib/campaign-protect.js +75 -0
- package/lib/campaign-reading.js +147 -0
- package/lib/campaign-schema.js +196 -0
- package/lib/campaign-store.js +380 -0
- package/lib/campaign-sync.js +302 -0
- package/lib/campaign-trace.js +300 -0
- package/lib/conductor-artifacts.js +15 -1
- package/lib/daily-log.js +11 -1
- package/lib/dispatch-scout.js +18 -0
- package/lib/engine-ask.js +35 -4
- package/lib/engine-registry.js +2 -0
- package/lib/engine-resource.js +518 -0
- package/lib/engine-validate.js +189 -9
- package/lib/fleet.js +125 -16
- package/lib/init-commit.js +80 -0
- package/lib/intents.js +8 -0
- package/lib/known-commands.js +1 -1
- package/lib/land-green.js +58 -7
- package/lib/landing-proof.js +352 -0
- package/lib/member-autoland.js +479 -10
- package/lib/member-board.js +86 -10
- package/lib/member-map-fold.js +126 -0
- package/lib/member-map-inbox.js +80 -0
- package/lib/member-progress.js +48 -1
- package/lib/member-report.js +433 -0
- package/lib/member-scaffold.js +1 -2
- package/lib/member-self-next-job.js +97 -0
- package/lib/member-switches.js +7 -0
- package/lib/member-task-pick.js +41 -14
- package/lib/member-up.js +249 -16
- package/lib/roster-prep.js +25 -2
- package/lib/runner-command.js +5 -0
- package/lib/task-db.js +63 -11
- package/lib/task-explanation.js +7 -0
- package/lib/task-list-keeper.js +8 -1
- package/lib/task-receipt.js +6 -1
- package/mcp/atris-mcp/index.mjs +212 -10
- package/package.json +2 -1
- package/scripts/det/engine-reading.js +37 -0
- package/scripts/det/ytnotes +29 -5
package/commands/autoland.js
CHANGED
|
@@ -9,6 +9,7 @@ const autoland = require('../lib/autoland');
|
|
|
9
9
|
const { gateForHuman, plainLandingReason } = require('../lib/voice-gate');
|
|
10
10
|
const { evaluateAutoAccept } = require('../lib/auto-accept-certified');
|
|
11
11
|
const { sweepEngineAskReceipts } = require('../lib/engine-receipt-sweep');
|
|
12
|
+
const { readSshAgents, findLeakedSshAgents, findAgentStartLines, agentStartLineText, stopLeakedSshAgents, closeLostRuns, readResourceHistory, errorReason, leakGrowthNote } = require('../lib/engine-resource');
|
|
12
13
|
const { validateRecentAskReceipts } = require('../lib/engine-validate');
|
|
13
14
|
const { operatorReady, hasAgentJargon, explainResult } = autoland;
|
|
14
15
|
const MISSION_AUTO_VERIFY_STATUSES = new Set(['planning', 'paused', 'ready']);
|
|
@@ -358,8 +359,8 @@ function refreshLandingRefs(root) {
|
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
|
|
361
|
-
function sweepLanding(root, { ttlDays, staleHours, now = Date.now() } = {}) {
|
|
362
|
-
refreshLandingRefs(root);
|
|
362
|
+
function sweepLanding(root, { ttlDays, staleHours, now = Date.now(), refresh = true } = {}) {
|
|
363
|
+
if (refresh) refreshLandingRefs(root);
|
|
363
364
|
const { collectBoard, reap } = require('./land');
|
|
364
365
|
// light: this board only feeds .branches staleness + .summary; reap builds
|
|
365
366
|
// its own full board for dirty-count salvage decisions.
|
|
@@ -479,7 +480,7 @@ function protectedReviewWaiting(root, policy = {}, waiting = []) {
|
|
|
479
480
|
// lib/voice-gate.js (plainLandingReason); this alias keeps call sites short.
|
|
480
481
|
const plainReason = plainLandingReason;
|
|
481
482
|
|
|
482
|
-
function showStatus(root, args) {
|
|
483
|
+
function showStatus(root, args, deps = {}) {
|
|
483
484
|
const json = args.includes('--json');
|
|
484
485
|
const policy = autoland.readPolicy(root);
|
|
485
486
|
const enabled = Boolean(policy && policy.enabled);
|
|
@@ -528,6 +529,45 @@ function showStatus(root, args) {
|
|
|
528
529
|
if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
|
|
529
530
|
const reapTrouble = autoland.readState(root).last_reap_error;
|
|
530
531
|
if (reapTrouble) console.log(` cleanup trouble: landing sweep failed on ${reapTrouble.date} (${reapTrouble.error}) - run: atris land --reap`);
|
|
532
|
+
const lost = readResourceHistory(root);
|
|
533
|
+
if (lost.read_error) console.log(` engine run log: could not read it (${lost.read_error}), so lost, killed, and failed runs in the last day are unknown`);
|
|
534
|
+
if (lost.lost_closed_day) {
|
|
535
|
+
const newest = lost.last_lost_closed;
|
|
536
|
+
const when = `${String(newest.at || '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
537
|
+
console.log(` engine runs closed after their parent died in the last day: ${lost.lost_closed_day}, newest ${newest.engine || 'unknown engine'} on ${newest.task || 'no task'} at ${when}`);
|
|
538
|
+
}
|
|
539
|
+
if (lost.kills_day) {
|
|
540
|
+
// Timeouts are counted apart, and the newest kill named is the newest one
|
|
541
|
+
// that was not a timeout, so a night of timeouts cannot hide a crash.
|
|
542
|
+
const timeLimit = lost.time_limit_kills_day;
|
|
543
|
+
const counts = timeLimit === lost.kills_day
|
|
544
|
+
? 'all at their time limit'
|
|
545
|
+
: `${timeLimit ? `${timeLimit} at their time limit, ` : ''}${lost.low_memory_kills_day} with machine memory low`;
|
|
546
|
+
const kill = lost.last_day_other_kill || lost.last_kill;
|
|
547
|
+
const label = timeLimit && lost.last_day_other_kill ? 'newest not at a time limit:' : 'newest';
|
|
548
|
+
const when = `${String(kill.at || '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
549
|
+
const free = Number.isFinite(kill.free_pct) ? `, ${kill.free_pct}% memory free` : '';
|
|
550
|
+
console.log(` engine runs force-killed in the last day: ${lost.kills_day}, ${counts}; ${label} ${kill.engine || 'unknown engine'} on ${kill.task || 'no task'} at ${when}, ${kill.cause || 'cause unknown'}${free}`);
|
|
551
|
+
}
|
|
552
|
+
if (lost.errors_day) {
|
|
553
|
+
// Runs that failed with a message and were not killed, such as an engine
|
|
554
|
+
// that could not start; kills are counted on the line above.
|
|
555
|
+
const failed = lost.last_day_error;
|
|
556
|
+
const when = `${String(failed.at || '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
557
|
+
console.log(` engine runs that ended in an error in the last day: ${lost.errors_day}, newest ${failed.engine || 'unknown engine'} on ${failed.task || 'no task'} at ${when}: ${errorReason(failed)}`);
|
|
558
|
+
}
|
|
559
|
+
const { leaked: leakedAgents, unread: unreadAgents } = countLeakedSshAgents(deps);
|
|
560
|
+
const unchecked = unreadAgents.length ? `could not read the ${unreadAgents.join(' or the ')}, so some ssh-agents went unchecked` : '';
|
|
561
|
+
if (leakedAgents) {
|
|
562
|
+
const stop = heartbeatLive ? 'the next hourly pass stops them' : 'stop them now: atris engine sweep --apply';
|
|
563
|
+
console.log(` leaked ssh-agents on this machine: ${unchecked ? 'at least ' : ''}${leakedAgents}${leakGrowthNote(lost.first_day_leak_reading, leakedAgents)}; ${stop}${unchecked ? `; ${unchecked}` : ''}`);
|
|
564
|
+
// A test run reads only the home it was handed, never the real one.
|
|
565
|
+
if (deps.home || !process.env.NODE_TEST_CONTEXT) {
|
|
566
|
+
for (const found of findAgentStartLines(deps.home ? { home: deps.home } : {})) console.log(` ${agentStartLineText(found)}`);
|
|
567
|
+
}
|
|
568
|
+
} else if (unchecked) {
|
|
569
|
+
console.log(` leaked ssh-agents on this machine: unknown, ${unchecked}`);
|
|
570
|
+
}
|
|
531
571
|
console.log('');
|
|
532
572
|
const humanOnly = blocked.filter((r) => String(r.reason || '').startsWith('denied_tag_'));
|
|
533
573
|
const needsWork = blocked.filter((r) => !String(r.reason || '').startsWith('denied_tag_'));
|
|
@@ -747,11 +787,20 @@ function persistTickReceipt(root, receipt) {
|
|
|
747
787
|
// Heartbeat liveness helpers live in lib/autoland.js so task ready copy and
|
|
748
788
|
// status share one gate (recent tick receipt within two hours).
|
|
749
789
|
|
|
790
|
+
function acceptedNotLandedNote(receipt) {
|
|
791
|
+
const rows = Array.isArray(receipt.accepted_not_landed) ? receipt.accepted_not_landed : [];
|
|
792
|
+
if (!rows.length) return '';
|
|
793
|
+
return `, ${rows.length} accepted but not proven landed (${rows.map((r) => `${r.ref} ${r.status}`).join(', ')})`;
|
|
794
|
+
}
|
|
795
|
+
|
|
750
796
|
function stuckBacklogNote(receipt) {
|
|
751
797
|
const parts = [];
|
|
752
798
|
const scanned = Number(receipt.scanned || 0);
|
|
753
799
|
const certified = Number(receipt.certified || 0);
|
|
754
|
-
|
|
800
|
+
// Stuck means nothing cleared review. Accepted-but-unproven work is named
|
|
801
|
+
// separately (accepted_not_landed), not misread as a wedged queue.
|
|
802
|
+
const accepted = Array.isArray(receipt.accepted) ? receipt.accepted.length : receipt.landed.length;
|
|
803
|
+
if (scanned > 0 && accepted === 0) {
|
|
755
804
|
parts.push(`${certified}/${scanned} certified in review, none landed`);
|
|
756
805
|
}
|
|
757
806
|
const unrunnable = Array.isArray(receipt.unrunnable_verifiers) ? receipt.unrunnable_verifiers.length : 0;
|
|
@@ -773,6 +822,85 @@ function engineReceiptSweepNote(receipt) {
|
|
|
773
822
|
return `, finalized ${finalized} stale engine receipt${finalized === 1 ? '' : 's'} as presumed dead`;
|
|
774
823
|
}
|
|
775
824
|
|
|
825
|
+
// Read only: how many ssh-agents look leaked right now, and which readers
|
|
826
|
+
// failed, since then the count is only a floor. A test run never reads the
|
|
827
|
+
// real machine, and a thrown read counts as none.
|
|
828
|
+
function countLeakedSshAgents(deps = {}) {
|
|
829
|
+
const none = { leaked: 0, unread: [] };
|
|
830
|
+
try {
|
|
831
|
+
const read = deps.readSshAgents;
|
|
832
|
+
if (!read && process.env.NODE_TEST_CONTEXT) return none;
|
|
833
|
+
const uid = deps.uid !== undefined ? deps.uid : (typeof process.getuid === 'function' ? process.getuid() : null);
|
|
834
|
+
if (uid === null) return none;
|
|
835
|
+
const { agents, referenced, unread = [] } = (read || readSshAgents)();
|
|
836
|
+
return { leaked: findLeakedSshAgents(agents, { referenced, uid }).length, unread };
|
|
837
|
+
} catch {
|
|
838
|
+
return none;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Shell startup files that start an ssh-agent per shell leave one behind
|
|
843
|
+
// for every engine shell. Each pass stops the ones a fresh recheck still
|
|
844
|
+
// calls leaked, and never lets a failed read break the pass.
|
|
845
|
+
function runSshAgentSweep(root, receipt, deps = {}) {
|
|
846
|
+
try {
|
|
847
|
+
const read = deps.read || readSshAgents;
|
|
848
|
+
const uid = deps.uid !== undefined ? deps.uid : (typeof process.getuid === 'function' ? process.getuid() : null);
|
|
849
|
+
const empty = { stopped: [], failed: [], skipped: [] };
|
|
850
|
+
// A test run must never stop real agents on the machine running it.
|
|
851
|
+
if (uid === null || (!deps.read && process.env.NODE_TEST_CONTEXT)) {
|
|
852
|
+
receipt.ssh_agent_sweep = empty;
|
|
853
|
+
return receipt.ssh_agent_sweep;
|
|
854
|
+
}
|
|
855
|
+
const { agents, referenced, unread = [] } = read();
|
|
856
|
+
const leaked = findLeakedSshAgents(agents, { referenced, uid });
|
|
857
|
+
const result = leaked.length
|
|
858
|
+
? stopLeakedSshAgents(root, leaked, { read, uid, ...(deps.kill ? { kill: deps.kill } : {}) })
|
|
859
|
+
: empty;
|
|
860
|
+
// A reading that failed on either scan is kept, so the summary can say
|
|
861
|
+
// some agents went unchecked instead of reading as a clean machine.
|
|
862
|
+
const missed = [...new Set([...unread, ...(result.unread || [])])];
|
|
863
|
+
receipt.ssh_agent_sweep = missed.length ? { ...result, unread: missed } : result;
|
|
864
|
+
} catch (err) {
|
|
865
|
+
receipt.ssh_agent_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
866
|
+
}
|
|
867
|
+
return receipt.ssh_agent_sweep || null;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function sshAgentSweepNote(receipt) {
|
|
871
|
+
const stopped = (receipt.ssh_agent_sweep?.stopped || []).length;
|
|
872
|
+
const failed = (receipt.ssh_agent_sweep?.failed || []).length;
|
|
873
|
+
const replaced = (receipt.ssh_agent_sweep?.replaced || []).length;
|
|
874
|
+
const unread = receipt.ssh_agent_sweep?.unread || [];
|
|
875
|
+
const stoppedNote = stopped ? `, stopped ${stopped} leaked ssh-agent${stopped === 1 ? '' : 's'}` : '';
|
|
876
|
+
const failedNote = failed ? `, could not stop ${failed} leaked ssh-agent${failed === 1 ? '' : 's'}` : '';
|
|
877
|
+
const replacedNote = replaced ? `, left ${replaced} younger replacement ssh-agent${replaced === 1 ? '' : 's'} alone` : '';
|
|
878
|
+
const unreadNote = unread.length ? `, could not read the ${unread.join(' or the ')}, so some ssh-agents went unchecked` : '';
|
|
879
|
+
const scanErrorNote = receipt.ssh_agent_sweep_error ? ', could not scan ssh-agents, so leaked agents may remain' : '';
|
|
880
|
+
return stoppedNote + failedNote + replacedNote + unreadNote + scanErrorNote;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// An engine run whose dispatching process died never writes its own end.
|
|
884
|
+
// Each pass writes a "parent gone" end for those, so the log stops carrying
|
|
885
|
+
// them as open, and never lets a failed read break the pass.
|
|
886
|
+
function runLostRunSweep(root, receipt, deps = {}) {
|
|
887
|
+
try {
|
|
888
|
+
const close = deps.close || closeLostRuns;
|
|
889
|
+
const closed = close(root, deps.now ? { now: deps.now } : {});
|
|
890
|
+
receipt.lost_run_sweep = { closed: closed.map((end) => end.run_id) };
|
|
891
|
+
} catch (err) {
|
|
892
|
+
receipt.lost_run_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
893
|
+
}
|
|
894
|
+
return receipt.lost_run_sweep || null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function lostRunSweepNote(receipt) {
|
|
898
|
+
const closed = (receipt.lost_run_sweep?.closed || []).length;
|
|
899
|
+
const closedNote = closed ? `, closed ${closed} lost engine run${closed === 1 ? '' : 's'}` : '';
|
|
900
|
+
const errorNote = receipt.lost_run_sweep_error ? ', could not close lost engine runs, so unfinished records may remain' : '';
|
|
901
|
+
return closedNote + errorNote;
|
|
902
|
+
}
|
|
903
|
+
|
|
776
904
|
async function runEngineAnswerValidation(root, receipt, deps = {}) {
|
|
777
905
|
try {
|
|
778
906
|
receipt.engine_answer_validation = await validateRecentAskReceipts(root, deps);
|
|
@@ -796,6 +924,9 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
796
924
|
receipt.engine_receipt_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
797
925
|
}
|
|
798
926
|
|
|
927
|
+
runSshAgentSweep(root, receipt);
|
|
928
|
+
runLostRunSweep(root, receipt);
|
|
929
|
+
|
|
799
930
|
await runEngineAnswerValidation(root, receipt, engineValidationDeps);
|
|
800
931
|
|
|
801
932
|
// 1. certify and land in one task process. Keeping both phases together lets
|
|
@@ -806,6 +937,9 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
806
937
|
// 78 certified rows waiting. Let `atris task auto-accept-certified` apply
|
|
807
938
|
// its own default (12 without --all, a high safety cap under --all) so a
|
|
808
939
|
// policy with accept_all:true actually drains the full certified set.
|
|
940
|
+
// Refresh origin once, before anything is accepted: the landing proof reads
|
|
941
|
+
// origin's target, and a stale ref would call landed work unlanded.
|
|
942
|
+
refreshLandingRefs(root);
|
|
809
943
|
const cliArgs = ['task', 'auto-accept-certified', '--json', '--certify-first'];
|
|
810
944
|
if (policy.accept_all) cliArgs.push('--all');
|
|
811
945
|
else if (policy.strict_verify === false) cliArgs.push('--no-strict-verify');
|
|
@@ -829,7 +963,21 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
829
963
|
if (parsed.certification && parsed.certification.ok !== true) {
|
|
830
964
|
receipt.certify_error = 'certify-verified failed';
|
|
831
965
|
}
|
|
832
|
-
|
|
966
|
+
// Accepted is a review verdict; landed is a git fact. Only work whose
|
|
967
|
+
// landing proof passed (lib/landing-proof.js) may be called landed. The
|
|
968
|
+
// rest is named with the reason, so an accept over an empty or unpushed
|
|
969
|
+
// commit reads as what it is instead of padding the landed count.
|
|
970
|
+
const acceptedRows = results.filter((r) => r.action === 'accepted');
|
|
971
|
+
receipt.accepted = acceptedRows.map((r) => r.ref);
|
|
972
|
+
receipt.landed = acceptedRows.filter((r) => r.landing && r.landing.status === 'landed').map((r) => r.ref);
|
|
973
|
+
const notLanded = acceptedRows
|
|
974
|
+
.filter((r) => !r.landing || r.landing.status !== 'landed')
|
|
975
|
+
.map((r) => ({
|
|
976
|
+
ref: r.ref,
|
|
977
|
+
status: (r.landing && r.landing.status) || 'unverified',
|
|
978
|
+
reason: (r.landing && r.landing.reason) || 'no_landing_proof',
|
|
979
|
+
}));
|
|
980
|
+
if (notLanded.length) receipt.accepted_not_landed = notLanded;
|
|
833
981
|
const citationBlocks = results
|
|
834
982
|
.filter((r) => r.action === 'skipped' && r.proof_state === 'suite_green_citation_required')
|
|
835
983
|
.map((r) => ({ ref: r.ref, reason: r.reason }));
|
|
@@ -964,7 +1112,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
964
1112
|
// TTL-expired work is salvaged before deletion, and 48h-stale active work
|
|
965
1113
|
// is carried into the digest instead of silently aging forever.
|
|
966
1114
|
try {
|
|
967
|
-
const landingSweep = sweepLanding(root);
|
|
1115
|
+
const landingSweep = sweepLanding(root, { refresh: false });
|
|
968
1116
|
receipt.reaped = landingSweep.reaped;
|
|
969
1117
|
receipt.landing_sweep = {
|
|
970
1118
|
before: landingSweep.before,
|
|
@@ -1035,6 +1183,22 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1035
1183
|
receipt.wish_dispatch_error = String((err && err.message) || err).slice(0, 200);
|
|
1036
1184
|
}
|
|
1037
1185
|
|
|
1186
|
+
// 3e. map fold, once an hour: map notes member changes saved as their own
|
|
1187
|
+
// files under atris/refs/map-notes.d/ are folded into the shared map notes
|
|
1188
|
+
// on main and removed, in one pushed commit. Off by policy: map_fold:false.
|
|
1189
|
+
if (policy.map_fold !== false) {
|
|
1190
|
+
try {
|
|
1191
|
+
const { foldDue, foldMapNotesOnMain } = require('../lib/member-map-fold');
|
|
1192
|
+
if (foldDue(state.last_map_fold_at)) {
|
|
1193
|
+
const fold = foldMapNotesOnMain(root);
|
|
1194
|
+
receipt.map_fold = { folded: fold.folded, added: fold.added || 0, reason: fold.reason || null };
|
|
1195
|
+
if (fold.reason !== 'main_unreadable') state.last_map_fold_at = new Date().toISOString();
|
|
1196
|
+
}
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
receipt.map_fold_error = String((err && err.message) || err).slice(0, 200);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1038
1202
|
// 4. daily digest at the configured hour
|
|
1039
1203
|
const today = new Date().toISOString().slice(0, 10);
|
|
1040
1204
|
const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
|
|
@@ -1148,7 +1312,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1148
1312
|
// Always print the standing backlog, and name work that cleared every
|
|
1149
1313
|
// gate and still did not land. That is the shape of a stuck loop.
|
|
1150
1314
|
const stuckNote = stuckBacklogNote(receipt);
|
|
1151
|
-
const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${engineSweepNote}${receiptNote}`;
|
|
1315
|
+
const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}${acceptedNotLandedNote(receipt)}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${engineSweepNote}${sshAgentSweepNote(receipt)}${lostRunSweepNote(receipt)}${receiptNote}`;
|
|
1152
1316
|
const validationLine = engineAnswerValidationLine(receipt);
|
|
1153
1317
|
if (validationLine) console.log(validationLine);
|
|
1154
1318
|
console.log(gateForHuman(summary).text);
|
|
@@ -1221,6 +1385,11 @@ module.exports = {
|
|
|
1221
1385
|
hasAgentJargon,
|
|
1222
1386
|
runEngineAnswerValidation,
|
|
1223
1387
|
runTickBody,
|
|
1388
|
+
runSshAgentSweep,
|
|
1389
|
+
sshAgentSweepNote,
|
|
1390
|
+
showStatus,
|
|
1391
|
+
runLostRunSweep,
|
|
1392
|
+
lostRunSweepNote,
|
|
1224
1393
|
sweepLanding,
|
|
1225
1394
|
verifyClosedTaskMissions,
|
|
1226
1395
|
};
|
package/commands/autopilot.js
CHANGED
|
@@ -119,7 +119,7 @@ function repoMapAuditReportsClean(cwd) {
|
|
|
119
119
|
* Scan workspace for the next thing worth doing.
|
|
120
120
|
* Returns { task, why, kind } or null.
|
|
121
121
|
*/
|
|
122
|
-
async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {}) {
|
|
122
|
+
async function suggestNextTask(cwd, skipped = new Set(), { auto = false, problems = null } = {}) {
|
|
123
123
|
const atrisDir = path.join(cwd, 'atris');
|
|
124
124
|
const suggestions = [];
|
|
125
125
|
|
|
@@ -344,7 +344,9 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
|
|
|
344
344
|
kind: 'imagined',
|
|
345
345
|
priority: 99
|
|
346
346
|
};
|
|
347
|
-
} catch {
|
|
347
|
+
} catch (err) {
|
|
348
|
+
// An engine that never answered is not an idle workspace; tell the caller.
|
|
349
|
+
if (problems && err && err.launchFailed) problems.push(String(err.message || '').split('\n')[0]);
|
|
348
350
|
return null;
|
|
349
351
|
}
|
|
350
352
|
}
|
|
@@ -607,6 +609,8 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
607
609
|
|
|
608
610
|
let rosterRunner = null;
|
|
609
611
|
let startedMs = Date.now();
|
|
612
|
+
// Prep writes its own pair of readings inside the pass, before this launch's.
|
|
613
|
+
let endResource = () => null;
|
|
610
614
|
try {
|
|
611
615
|
let cmd = options.cmdOverride;
|
|
612
616
|
if (!cmd) {
|
|
@@ -638,6 +642,12 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
638
642
|
const env = { ...process.env };
|
|
639
643
|
delete env.CLAUDECODE;
|
|
640
644
|
startedMs = Date.now();
|
|
645
|
+
// Machine memory and load at both ends of the launch, so a phase killed
|
|
646
|
+
// with SIGKILL says whether the machine was short on memory.
|
|
647
|
+
endResource = require('../lib/engine-resource').startEngineRun(process.cwd(), {
|
|
648
|
+
engine: (rosterRunner && rosterRunner.engineId) || 'runner',
|
|
649
|
+
task: `autopilot ${phase}`,
|
|
650
|
+
});
|
|
641
651
|
const output = execPhaseCommandSync(cmd, {
|
|
642
652
|
cwd: process.cwd(),
|
|
643
653
|
encoding: 'utf8',
|
|
@@ -648,10 +658,12 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
648
658
|
});
|
|
649
659
|
|
|
650
660
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
661
|
+
endResource();
|
|
651
662
|
recordPhaseRosterRun(process.cwd(), phase, rosterRunner, startedMs, { outcome: 'landed', stdout: output || '' });
|
|
652
663
|
return { prompt, output: output || '' };
|
|
653
664
|
} catch (err) {
|
|
654
665
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
666
|
+
const resourceEnd = endResource(err);
|
|
655
667
|
recordPhaseRosterRun(process.cwd(), phase, rosterRunner, startedMs, phaseFailureOutcome(err, rosterRunner), err);
|
|
656
668
|
if (isPhaseTimeoutError(err)) {
|
|
657
669
|
// A roster worker that stalls sits out the cooldown, so the next
|
|
@@ -664,7 +676,8 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
664
676
|
throw new Error(`${phase} phase timed out after ${timeout / 1000}s (configured runner hit the wall; any work it committed survives, reconcile from pre-tick HEADs)`);
|
|
665
677
|
}
|
|
666
678
|
if (isPhaseKillError(err)) {
|
|
667
|
-
|
|
679
|
+
const cause = resourceEnd && resourceEnd.cause ? `${resourceEnd.cause}; ` : '';
|
|
680
|
+
throw new Error(`${phase} phase killed by ${err.signal || 'a signal'} before the ${timeout / 1000}s wall, not a timeout; ${cause}check memory pressure or an external supervisor`);
|
|
668
681
|
}
|
|
669
682
|
if (err.stdout) {
|
|
670
683
|
return { prompt, output: err.stdout };
|
|
@@ -1525,6 +1538,7 @@ function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1525
1538
|
fs.writeFileSync(tmpFile, prompt);
|
|
1526
1539
|
let runner = null;
|
|
1527
1540
|
let startedMs = Date.now();
|
|
1541
|
+
let endResource = () => null;
|
|
1528
1542
|
try {
|
|
1529
1543
|
runner = buildPlanReviewRunner(tmpFile, cwd, timeout);
|
|
1530
1544
|
const cmd = runner.command;
|
|
@@ -1532,6 +1546,12 @@ function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1532
1546
|
const env = { ...process.env };
|
|
1533
1547
|
delete env.CLAUDECODE;
|
|
1534
1548
|
startedMs = Date.now();
|
|
1549
|
+
// Machine memory and load at both ends of the review, so one killed with
|
|
1550
|
+
// SIGKILL says whether the machine was short on memory.
|
|
1551
|
+
endResource = require('../lib/engine-resource').startEngineRun(cwd, {
|
|
1552
|
+
engine: (runner && runner.engineId) || 'runner',
|
|
1553
|
+
task: 'autopilot plan review',
|
|
1554
|
+
});
|
|
1535
1555
|
const output = execPhaseCommandSync(cmd, {
|
|
1536
1556
|
cwd,
|
|
1537
1557
|
encoding: 'utf8',
|
|
@@ -1540,11 +1560,15 @@ function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1540
1560
|
maxBuffer: 10 * 1024 * 1024,
|
|
1541
1561
|
env,
|
|
1542
1562
|
});
|
|
1563
|
+
endResource();
|
|
1543
1564
|
recordPhaseRosterRun(cwd, 'plan review', runner, startedMs, { outcome: 'landed', stdout: output || '' });
|
|
1544
1565
|
return output || '';
|
|
1545
1566
|
} catch (err) {
|
|
1567
|
+
const end = endResource(err);
|
|
1546
1568
|
recordPhaseRosterRun(cwd, 'plan review', runner, startedMs, phaseFailureOutcome(err, runner), err);
|
|
1547
1569
|
if (err.stdout) return err.stdout;
|
|
1570
|
+
// A kill leaves no report of its own, so the error names the likely cause.
|
|
1571
|
+
if (end && end.cause && err instanceof Error) err.message = `${err.message}; ${end.cause}`;
|
|
1548
1572
|
throw err;
|
|
1549
1573
|
} finally {
|
|
1550
1574
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
@@ -1557,6 +1581,9 @@ function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1557
1581
|
*/
|
|
1558
1582
|
function defaultCodexExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
1559
1583
|
const cmd = process.env.ATRIS_CODEX_CMD || 'codex';
|
|
1584
|
+
// Machine memory and load at both ends of the signer, so one killed with
|
|
1585
|
+
// SIGKILL says whether the machine was short on memory.
|
|
1586
|
+
const endResource = require('../lib/engine-resource').startEngineRun(cwd, { engine: 'codex', task: 'autopilot codex review' });
|
|
1560
1587
|
const proc = spawnSync(cmd, ['-p', prompt], {
|
|
1561
1588
|
cwd,
|
|
1562
1589
|
encoding: 'utf8',
|
|
@@ -1575,8 +1602,18 @@ function defaultCodexExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1575
1602
|
if (sweepErr.code !== 'ESRCH') throw sweepErr;
|
|
1576
1603
|
}
|
|
1577
1604
|
}
|
|
1605
|
+
// spawnSync does not throw, so a failure is passed in the thrown shape.
|
|
1606
|
+
const failed = proc.error || proc.signal || proc.status !== 0;
|
|
1607
|
+
const end = endResource(failed ? {
|
|
1608
|
+
code: proc.error && proc.error.code,
|
|
1609
|
+
signal: proc.signal,
|
|
1610
|
+
stderr: proc.stderr,
|
|
1611
|
+
message: proc.error ? proc.error.message : `codex exited with status ${proc.status}`,
|
|
1612
|
+
} : null);
|
|
1613
|
+
const cause = end && end.cause ? `; ${end.cause}` : '';
|
|
1578
1614
|
if (proc.status !== 0 && !proc.stdout) {
|
|
1579
|
-
|
|
1615
|
+
const how = proc.signal ? `was stopped by ${proc.signal}` : `exited with status ${proc.status}`;
|
|
1616
|
+
throw new Error(`codex ${how}: ${proc.stderr || 'no output'}${cause}`);
|
|
1580
1617
|
}
|
|
1581
1618
|
return proc.stdout || '';
|
|
1582
1619
|
}
|
|
@@ -3268,10 +3305,14 @@ Reply with the JSON array and nothing else.`;
|
|
|
3268
3305
|
fs.writeFileSync(tmpFile, prompt);
|
|
3269
3306
|
|
|
3270
3307
|
let output = '';
|
|
3308
|
+
let endResource = () => null;
|
|
3271
3309
|
try {
|
|
3272
3310
|
const cmd = buildRunnerCommand({ promptFile: tmpFile });
|
|
3273
3311
|
const env = { ...process.env };
|
|
3274
3312
|
delete env.CLAUDECODE;
|
|
3313
|
+
// Machine memory and load at both ends of the proposal, so one killed
|
|
3314
|
+
// with SIGKILL says whether the machine was short on memory.
|
|
3315
|
+
endResource = require('../lib/engine-resource').startEngineRun(cwd, { engine: 'runner', task: 'horizon proposal' });
|
|
3275
3316
|
output = execPhaseCommandSync(cmd, {
|
|
3276
3317
|
cwd,
|
|
3277
3318
|
encoding: 'utf8',
|
|
@@ -3280,13 +3321,18 @@ Reply with the JSON array and nothing else.`;
|
|
|
3280
3321
|
maxBuffer: 10 * 1024 * 1024,
|
|
3281
3322
|
env
|
|
3282
3323
|
}).toString();
|
|
3324
|
+
endResource();
|
|
3283
3325
|
} catch (err) {
|
|
3326
|
+
const end = endResource(err);
|
|
3327
|
+
// launchFailed marks errors where the engine never answered.
|
|
3284
3328
|
if (isPhaseTimeoutError(err)) {
|
|
3285
|
-
throw new Error(`horizon-proposal phase timed out after ${PHASE_TIMEOUT / 1000}s`);
|
|
3329
|
+
throw Object.assign(new Error(`horizon-proposal phase timed out after ${PHASE_TIMEOUT / 1000}s`), { launchFailed: true });
|
|
3286
3330
|
}
|
|
3287
3331
|
if (isPhaseKillError(err)) {
|
|
3288
|
-
|
|
3332
|
+
const cause = end && end.cause ? `; ${end.cause}` : '';
|
|
3333
|
+
throw Object.assign(new Error(`horizon-proposal phase killed by ${err.signal || 'a signal'} before the ${PHASE_TIMEOUT / 1000}s wall, not a timeout${cause}`), { launchFailed: true });
|
|
3289
3334
|
}
|
|
3335
|
+
if (err && typeof err === 'object') err.launchFailed = true;
|
|
3290
3336
|
throw err;
|
|
3291
3337
|
} finally {
|
|
3292
3338
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
@@ -3470,18 +3516,23 @@ async function autopilotAtris(description, options = {}) {
|
|
|
3470
3516
|
break;
|
|
3471
3517
|
}
|
|
3472
3518
|
|
|
3473
|
-
const
|
|
3519
|
+
const problems = [];
|
|
3520
|
+
const suggestion = await suggestNextTask(cwd, skipped, { auto, problems });
|
|
3474
3521
|
|
|
3475
3522
|
if (!suggestion) {
|
|
3523
|
+
// A failed proposal launch must not read as a clean workspace.
|
|
3524
|
+
const proposalFailure = problems[0] ? `I could not ask the engine for new work: ${problems[0]}` : '';
|
|
3476
3525
|
tickOutcome = 'idle';
|
|
3477
|
-
tickOutcomeText =
|
|
3526
|
+
tickOutcomeText = proposalFailure
|
|
3527
|
+
? `I found no work to pick up this tick. ${proposalFailure}`
|
|
3528
|
+
: 'I checked the repo and found no work to pick up this tick.';
|
|
3478
3529
|
tickNextStep = 'scan for new signals and propose the next horizon';
|
|
3479
3530
|
if (verbose) {
|
|
3480
|
-
console.log(' nothing to do. workspace is clean.');
|
|
3531
|
+
console.log(proposalFailure ? ` nothing to do. ${proposalFailure}` : ' nothing to do. workspace is clean.');
|
|
3481
3532
|
} else {
|
|
3482
3533
|
printPlainBlock([
|
|
3483
3534
|
'I found no work this tick.',
|
|
3484
|
-
'The workspace looks clean.',
|
|
3535
|
+
proposalFailure || 'The workspace looks clean.',
|
|
3485
3536
|
'',
|
|
3486
3537
|
'Next I will stop until a new signal appears.'
|
|
3487
3538
|
].join('\n'));
|
|
@@ -3956,10 +4007,14 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
|
|
|
3956
4007
|
const tmpFile = path.join(cwd, '.staleness-prompt.tmp');
|
|
3957
4008
|
fs.writeFileSync(tmpFile, prompt);
|
|
3958
4009
|
|
|
4010
|
+
let endResource = () => null;
|
|
3959
4011
|
try {
|
|
3960
4012
|
const env = { ...process.env };
|
|
3961
4013
|
delete env.CLAUDECODE;
|
|
3962
4014
|
const cmd = buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Glob,Grep' });
|
|
4015
|
+
// Machine memory and load at both ends of the check, so one killed with
|
|
4016
|
+
// SIGKILL says whether the machine was short on memory.
|
|
4017
|
+
endResource = require('../lib/engine-resource').startEngineRun(cwd, { engine: 'runner', task: 'staleness check' });
|
|
3963
4018
|
const output = execPhaseCommandSync(cmd, {
|
|
3964
4019
|
cwd,
|
|
3965
4020
|
encoding: 'utf8',
|
|
@@ -3968,6 +4023,7 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
|
|
|
3968
4023
|
maxBuffer: 2 * 1024 * 1024,
|
|
3969
4024
|
env
|
|
3970
4025
|
}).trim();
|
|
4026
|
+
endResource();
|
|
3971
4027
|
|
|
3972
4028
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
3973
4029
|
|
|
@@ -3980,7 +4036,10 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
|
|
|
3980
4036
|
} catch (err) {
|
|
3981
4037
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
3982
4038
|
// On timeout or crash, treat as unverifiable, conservative default
|
|
3983
|
-
|
|
4039
|
+
// A kill leaves no report of its own, so the reason names the likely cause.
|
|
4040
|
+
const end = endResource(err);
|
|
4041
|
+
const cause = end && end.cause ? `; ${end.cause}` : '';
|
|
4042
|
+
return { fresh: false, reasoning: `Model check failed: ${(err.message || '').slice(0, 100)}${cause}` };
|
|
3984
4043
|
}
|
|
3985
4044
|
}
|
|
3986
4045
|
|
|
@@ -3992,6 +4051,7 @@ async function autopilotFromTodo(options = {}) {
|
|
|
3992
4051
|
}
|
|
3993
4052
|
|
|
3994
4053
|
module.exports = {
|
|
4054
|
+
askModel,
|
|
3995
4055
|
snapshotRepoHeads,
|
|
3996
4056
|
diffAdvancedRepoHeads,
|
|
3997
4057
|
reconcileTimedOutTick,
|