atris 3.82.25 → 3.83.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/AGENTS.md +10 -12
- package/README.md +77 -0
- package/bin/atris.js +95 -44
- package/commands/activate.js +39 -33
- package/commands/agent-home.js +403 -0
- package/commands/agent-signin.js +214 -0
- package/commands/agent-spawn.js +1 -0
- package/commands/autoland.js +173 -71
- package/commands/brain.js +211 -410
- package/commands/doctor.js +38 -26
- package/commands/init.js +4 -4
- package/commands/join.js +159 -0
- package/commands/memory.js +2 -2
- package/commands/morning.js +132 -0
- package/commands/night.js +178 -0
- package/commands/recap.js +4 -4
- package/commands/task.js +31 -2
- package/commands/worktree.js +18 -0
- package/lib/agent-home.js +289 -0
- package/lib/agent-page.js +1257 -0
- package/lib/cleanup-summary.js +4 -2
- package/lib/first-minute.js +41 -20
- package/lib/gm-phone-page.js +8 -2
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +39 -13
- package/lib/member-export.js +31 -0
- package/lib/memory-folder.js +122 -24
- package/lib/memory-live.js +2 -2
- package/lib/night-learned.js +171 -0
- package/lib/night-shift.js +123 -0
- package/lib/task-core.js +2 -1
- package/lib/task-db.js +32 -0
- package/package.json +1 -1
- package/templates/members/designer-1/MEMBER.md +34 -0
- package/templates/members/designer-1/MISSION.md +11 -0
- package/templates/members/designer-1/SOUL.md +11 -0
- package/templates/members/designer-1/context/why.md +5 -0
- package/templates/members/designer-1/goals.json +5 -0
- package/templates/members/designer-1/goals.md +3 -0
- package/templates/members/designer-1/skills/designer-1/SKILL.md +179 -0
- package/templates/members/designer-1/skills/designer-1/parts.md +55 -0
package/commands/autoland.js
CHANGED
|
@@ -736,12 +736,17 @@ async function runTick(root, args) {
|
|
|
736
736
|
const json = args.includes('--json');
|
|
737
737
|
const policy = autoland.readPolicy(root);
|
|
738
738
|
const receipt = { at: new Date().toISOString(), landed: [], alarms: 0, digest_due: false, digest_sent: false, enabled: Boolean(policy && policy.enabled) };
|
|
739
|
+
// The clock starts before the off and lock checks. Every receipt this
|
|
740
|
+
// command prints carries step_ms, total_ms and ended_at, including the ones
|
|
741
|
+
// where nothing ran (step_ms is empty there).
|
|
742
|
+
const timer = createTickTimer(receipt);
|
|
739
743
|
if (!policy || policy.enabled !== true) {
|
|
744
|
+
timer.finish();
|
|
740
745
|
if (json) console.log(JSON.stringify(receipt));
|
|
741
746
|
else console.log('autoland is off; tick did nothing.');
|
|
742
747
|
return 0;
|
|
743
748
|
}
|
|
744
|
-
if (args.includes('--dry-run')) return runTickPreview(root, { json, receipt });
|
|
749
|
+
if (args.includes('--dry-run')) return runTickPreview(root, { json, receipt, timer });
|
|
745
750
|
|
|
746
751
|
// One live tick at a time: verify re-runs can take minutes each, and a
|
|
747
752
|
// second hourly cron firing into the same snapshot double-accepts,
|
|
@@ -756,12 +761,13 @@ async function runTick(root, args) {
|
|
|
756
761
|
}
|
|
757
762
|
if (lock && !lock.ok) {
|
|
758
763
|
receipt.skipped_reason = 'tick_already_running';
|
|
764
|
+
timer.finish();
|
|
759
765
|
if (json) console.log(JSON.stringify(receipt));
|
|
760
766
|
else console.log('autoland tick: another tick is still running; skipped.');
|
|
761
767
|
return 0;
|
|
762
768
|
}
|
|
763
769
|
try {
|
|
764
|
-
return await runTickBody(root, { json, policy, receipt });
|
|
770
|
+
return await runTickBody(root, { json, policy, receipt, timer });
|
|
765
771
|
} finally {
|
|
766
772
|
// Only removes the lock while it is still this tick's.
|
|
767
773
|
if (lock) lock.release();
|
|
@@ -771,7 +777,7 @@ async function runTick(root, args) {
|
|
|
771
777
|
// `atris autoland tick --dry-run`: what the hourly pass would close, send
|
|
772
778
|
// back, and check, read only. No lock, no receipt, no GitHub writes, no job
|
|
773
779
|
// changes, no fetch.
|
|
774
|
-
function runTickPreview(root, { json, receipt }) {
|
|
780
|
+
function runTickPreview(root, { json, receipt, timer }) {
|
|
775
781
|
receipt.dry_run = true;
|
|
776
782
|
runMergedWorkClose(root, receipt, { dryRun: true });
|
|
777
783
|
// The second check's next turn, in the order it would take the jobs.
|
|
@@ -786,6 +792,7 @@ function runTickPreview(root, { json, receipt }) {
|
|
|
786
792
|
} catch (err) {
|
|
787
793
|
receipt.second_check_error = String((err && err.message) || err).slice(0, 200);
|
|
788
794
|
}
|
|
795
|
+
if (timer) timer.finish();
|
|
789
796
|
if (json) {
|
|
790
797
|
console.log(JSON.stringify(receipt));
|
|
791
798
|
return 0;
|
|
@@ -1047,24 +1054,105 @@ function acceptCommandArgs(policy) {
|
|
|
1047
1054
|
return args;
|
|
1048
1055
|
}
|
|
1049
1056
|
|
|
1050
|
-
|
|
1057
|
+
// A tick that ran past five minutes left a receipt with no timing at all, so
|
|
1058
|
+
// nobody could say which step ate the time. The timer writes receipt.step_ms
|
|
1059
|
+
// (whole milliseconds per step), receipt.total_ms and receipt.ended_at.
|
|
1060
|
+
// A step that is switched off by policy is absent from step_ms, not zero: a
|
|
1061
|
+
// zero would read as "ran and was instant". A step that returns early or
|
|
1062
|
+
// throws still records its time. The clock is monotonic whole milliseconds:
|
|
1063
|
+
// the steps can never add up to more than the total.
|
|
1064
|
+
const SLOW_STEP_MS = 60 * 1000;
|
|
1065
|
+
|
|
1066
|
+
function monotonicMs() {
|
|
1067
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
1068
|
+
}
|
|
1051
1069
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1070
|
+
function createTickTimer(receipt, now = monotonicMs) {
|
|
1071
|
+
const started = now();
|
|
1072
|
+
const stepMs = {};
|
|
1073
|
+
receipt.step_ms = stepMs;
|
|
1074
|
+
const since = (from) => Math.max(0, Math.round(now() - from));
|
|
1075
|
+
const timed = (name, fn) => {
|
|
1076
|
+
const from = now();
|
|
1077
|
+
// Added, not assigned: a step name used twice keeps both runs.
|
|
1078
|
+
const record = () => { stepMs[name] = (stepMs[name] || 0) + since(from); };
|
|
1079
|
+
let out;
|
|
1080
|
+
try {
|
|
1081
|
+
out = fn();
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
record();
|
|
1084
|
+
throw err;
|
|
1085
|
+
}
|
|
1086
|
+
if (out && typeof out.then === 'function') {
|
|
1087
|
+
return out.then((value) => { record(); return value; }, (err) => { record(); throw err; });
|
|
1088
|
+
}
|
|
1089
|
+
record();
|
|
1090
|
+
return out;
|
|
1091
|
+
};
|
|
1092
|
+
const finish = () => {
|
|
1093
|
+
receipt.total_ms = since(started);
|
|
1094
|
+
receipt.ended_at = new Date().toISOString();
|
|
1095
|
+
};
|
|
1096
|
+
return { timed, finish };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function tookSeconds(ms) {
|
|
1100
|
+
const tenths = Math.round(Math.max(0, Number(ms) || 0) / 100) / 10;
|
|
1101
|
+
return `${tenths < 10 ? tenths.toFixed(1) : Math.round(tenths)}s`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// The longest single step over the line, or null. One step is named so the
|
|
1105
|
+
// line stays one line however many ran long.
|
|
1106
|
+
function slowestStep(receipt, thresholdMs = SLOW_STEP_MS) {
|
|
1107
|
+
let slow = null;
|
|
1108
|
+
for (const [name, ms] of Object.entries((receipt && receipt.step_ms) || {})) {
|
|
1109
|
+
if (Number.isFinite(ms) && ms > thresholdMs && (!slow || ms > slow.ms)) slow = { name, ms };
|
|
1062
1110
|
}
|
|
1111
|
+
return slow;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function tickTimingNote(receipt) {
|
|
1115
|
+
return Number.isFinite(receipt.total_ms) ? `, took ${tookSeconds(receipt.total_ms)}` : '';
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function slowStepLine(receipt, thresholdMs = SLOW_STEP_MS) {
|
|
1119
|
+
const slow = slowestStep(receipt, thresholdMs);
|
|
1120
|
+
return slow ? `slow step: ${slow.name} took ${tookSeconds(slow.ms)}` : '';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// One row of the receipt's green landing list. duration_ms is how long the
|
|
1124
|
+
// full verify took; 0 is a real value, a missing one stays missing.
|
|
1125
|
+
function greenVerdictRow(v) {
|
|
1126
|
+
const row = { branch: v.branch, action: v.action, reason: v.reason || null, detail: v.detail || null, failing: v.verify ? v.verify.failing : undefined };
|
|
1127
|
+
if (v.verify && Number.isFinite(v.verify.duration_ms)) row.duration_ms = v.verify.duration_ms;
|
|
1128
|
+
return row;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async function runTickBody(root, { json, policy, receipt, engineValidationDeps = {}, clock, timer }) {
|
|
1132
|
+
// runTick hands in the timer it started before the lock check. The total
|
|
1133
|
+
// covers the wait for the lock too. Called alone, the body starts its own.
|
|
1134
|
+
const { timed, finish } = timer || createTickTimer(receipt, clock);
|
|
1135
|
+
|
|
1136
|
+
timed('engine_receipt_sweep', () => {
|
|
1137
|
+
try {
|
|
1138
|
+
receipt.engine_receipt_sweep = sweepEngineAskReceipts(root);
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
receipt.engine_receipt_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
timed('fleet_receipt_sweep', () => {
|
|
1144
|
+
try {
|
|
1145
|
+
const fleetSweep = sweepFleetReceipts(root);
|
|
1146
|
+
if (fleetSweep.scanned > 0) receipt.fleet_receipt_sweep = fleetSweep;
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
receipt.fleet_receipt_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1063
1151
|
|
|
1064
|
-
runSshAgentSweep(root, receipt);
|
|
1065
|
-
runLostRunSweep(root, receipt);
|
|
1152
|
+
timed('ssh_agent_sweep', () => runSshAgentSweep(root, receipt));
|
|
1153
|
+
timed('lost_run_sweep', () => runLostRunSweep(root, receipt));
|
|
1066
1154
|
|
|
1067
|
-
await runEngineAnswerValidation(root, receipt, engineValidationDeps);
|
|
1155
|
+
await timed('engine_answer_validation', () => runEngineAnswerValidation(root, receipt, engineValidationDeps));
|
|
1068
1156
|
|
|
1069
1157
|
// 1. certify and land in one task process. Keeping both phases together lets
|
|
1070
1158
|
// the landing gate reuse the live certification verifier result without
|
|
@@ -1076,13 +1164,13 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1076
1164
|
// policy with accept_all:true actually drains the full certified set.
|
|
1077
1165
|
// Refresh origin once, before anything is accepted: the landing proof reads
|
|
1078
1166
|
// origin's target, and a stale ref would call landed work unlanded.
|
|
1079
|
-
refreshLandingRefs(root);
|
|
1167
|
+
timed('origin_refresh', () => refreshLandingRefs(root));
|
|
1080
1168
|
const cliArgs = acceptCommandArgs(policy);
|
|
1081
1169
|
const pendingReviewRows = readProjection(root)
|
|
1082
1170
|
.filter((task) => task && task.status === 'review')
|
|
1083
1171
|
.filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
|
|
1084
1172
|
.length;
|
|
1085
|
-
const accept = runOwnCli(root, cliArgs, acceptSweepTimeoutMs(pendingReviewRows));
|
|
1173
|
+
const accept = timed('accept_certify', () => runOwnCli(root, cliArgs, acceptSweepTimeoutMs(pendingReviewRows)));
|
|
1086
1174
|
let acceptResults = [];
|
|
1087
1175
|
try {
|
|
1088
1176
|
const parsed = JSON.parse(accept.stdout);
|
|
@@ -1148,7 +1236,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1148
1236
|
// 2a. merged work closes its jobs and clashing work goes back on the list,
|
|
1149
1237
|
// every hour, whether or not a team loop is running. After the accept step,
|
|
1150
1238
|
// so work that can be checked and landed the normal way is landed first.
|
|
1151
|
-
const mergedClose = runMergedWorkClose(root, receipt);
|
|
1239
|
+
const mergedClose = timed('merged_close', () => runMergedWorkClose(root, receipt));
|
|
1152
1240
|
if ((mergedClose.closed || []).length + (mergedClose.sent_back || []).length > 0) {
|
|
1153
1241
|
try { require('./task').refreshKeptTaskList(root); } catch {}
|
|
1154
1242
|
}
|
|
@@ -1157,7 +1245,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1157
1245
|
// sentence per piece (the day-one PM sentence written at finish time),
|
|
1158
1246
|
// capped at three with the rest counted. Off with live_updates: false.
|
|
1159
1247
|
const tasksForLive = readProjection(root);
|
|
1160
|
-
if (receipt.landed.length > 0 && policy.imessage_to && policy.live_updates !== false) {
|
|
1248
|
+
if (receipt.landed.length > 0 && policy.imessage_to && policy.live_updates !== false) timed('live_update', () => {
|
|
1161
1249
|
const text = autoland.composeLiveUpdate({
|
|
1162
1250
|
landedRefs: receipt.landed,
|
|
1163
1251
|
tasks: tasksForLive,
|
|
@@ -1171,7 +1259,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1171
1259
|
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
1172
1260
|
receipt.live_update_sent = sent.ok;
|
|
1173
1261
|
}
|
|
1174
|
-
}
|
|
1262
|
+
});
|
|
1175
1263
|
|
|
1176
1264
|
// 3. alarm on anything waiting on a human past the line
|
|
1177
1265
|
const state = autoland.readState(root);
|
|
@@ -1180,14 +1268,14 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1180
1268
|
const protectedWaiting = protectedReviewWaiting(root, policy, waiting);
|
|
1181
1269
|
const alarmHours = Number(policy.alarm_hours) || autoland.DEFAULT_ALARM_HOURS;
|
|
1182
1270
|
const due = autoland.dueForAlarm(waiting, state, { alarmHours });
|
|
1183
|
-
if (due.length > 0 && policy.imessage_to) {
|
|
1271
|
+
if (due.length > 0 && policy.imessage_to) timed('alarm', () => {
|
|
1184
1272
|
const text = autoland.composeAlarm({ waiting: due, project: projectName(root), alarmHours });
|
|
1185
1273
|
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
1186
1274
|
if (sent.ok) {
|
|
1187
1275
|
autoland.markAlerted(state, due);
|
|
1188
1276
|
receipt.alarms = due.length;
|
|
1189
1277
|
}
|
|
1190
|
-
}
|
|
1278
|
+
});
|
|
1191
1279
|
|
|
1192
1280
|
// 3b. janitor, every tick: a mission left paused past 48h and a worktree
|
|
1193
1281
|
// already merged into base are the same rot pattern as an unlanded branch,
|
|
@@ -1200,7 +1288,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1200
1288
|
receipt.missions_stopped = 0;
|
|
1201
1289
|
receipt.missions_held = 0;
|
|
1202
1290
|
receipt.worktrees_reaped = 0;
|
|
1203
|
-
if (policy.janitor !== false) {
|
|
1291
|
+
if (policy.janitor !== false) timed('janitor', () => {
|
|
1204
1292
|
let stoppedMissions = [];
|
|
1205
1293
|
let heldMissions = [];
|
|
1206
1294
|
try {
|
|
@@ -1227,13 +1315,13 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1227
1315
|
receipt.janitor_worktree_error = String((err && err.message) || err).slice(0, 200);
|
|
1228
1316
|
}
|
|
1229
1317
|
recordJanitorState(state, { stopped: stoppedMissions, held: heldMissions, worktrees: receipt.worktrees_reaped });
|
|
1230
|
-
}
|
|
1318
|
+
});
|
|
1231
1319
|
|
|
1232
1320
|
// 3b2. green landing, every tick: one stale branch that merges clean and
|
|
1233
1321
|
// passes the full verify in a scratch checkout lands on base and is pushed.
|
|
1234
1322
|
// A red branch opens a close flag the same hour instead of waiting for the
|
|
1235
1323
|
// TTL reap to salvage it. Off by policy: green_landing:false.
|
|
1236
|
-
if (policy.green_landing !== false) {
|
|
1324
|
+
if (policy.green_landing !== false) timed('green_landing', () => {
|
|
1237
1325
|
try {
|
|
1238
1326
|
const { landStaleGreenBranches, summaryLine } = require('../lib/land-green');
|
|
1239
1327
|
const green = landStaleGreenBranches(root, {
|
|
@@ -1244,35 +1332,37 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1244
1332
|
candidates: green.candidates.length,
|
|
1245
1333
|
landed: green.landed,
|
|
1246
1334
|
red: green.red,
|
|
1247
|
-
verdicts: green.verdicts.map(
|
|
1335
|
+
verdicts: green.verdicts.map(greenVerdictRow),
|
|
1248
1336
|
summary: summaryLine(green),
|
|
1249
1337
|
};
|
|
1250
1338
|
state.green_landing = { at: green.at, ...receipt.green_landing };
|
|
1251
1339
|
} catch (err) {
|
|
1252
1340
|
receipt.green_landing_error = String((err && err.message) || err).slice(0, 200);
|
|
1253
1341
|
}
|
|
1254
|
-
}
|
|
1342
|
+
});
|
|
1255
1343
|
|
|
1256
1344
|
// 3c. landing sweep, every tick: merged branch residue is cleared,
|
|
1257
1345
|
// TTL-expired work is salvaged before deletion, and 48h-stale active work
|
|
1258
1346
|
// is carried into the digest instead of silently aging forever.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1347
|
+
timed('landing_sweep', () => {
|
|
1348
|
+
try {
|
|
1349
|
+
const landingSweep = sweepLanding(root, { refresh: false });
|
|
1350
|
+
receipt.reaped = landingSweep.reaped;
|
|
1351
|
+
receipt.landing_sweep = {
|
|
1352
|
+
before: landingSweep.before,
|
|
1353
|
+
after: landingSweep.after,
|
|
1354
|
+
stale: landingSweep.stale.length,
|
|
1355
|
+
human: landingSweep.human.length,
|
|
1356
|
+
};
|
|
1357
|
+
if (landingSweep.reaped.bundleError) {
|
|
1358
|
+
receipt.reap_error = `backup failed, unlanded work kept in place: ${landingSweep.reaped.bundleError}`;
|
|
1359
|
+
}
|
|
1360
|
+
recordLandingSweepState(state, landingSweep);
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
receipt.reap_error = String((err && err.message) || err).slice(0, 200);
|
|
1363
|
+
recordLandingSweepState(state, null, { error: receipt.reap_error });
|
|
1270
1364
|
}
|
|
1271
|
-
|
|
1272
|
-
} catch (err) {
|
|
1273
|
-
receipt.reap_error = String((err && err.message) || err).slice(0, 200);
|
|
1274
|
-
recordLandingSweepState(state, null, { error: receipt.reap_error });
|
|
1275
|
-
}
|
|
1365
|
+
});
|
|
1276
1366
|
|
|
1277
1367
|
// 3c2. idle work resolves itself, every tick. Finished work that sat still
|
|
1278
1368
|
// past the grace period (policy.idle_close_days, default 7) and can never
|
|
@@ -1281,7 +1371,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1281
1371
|
// nights and trips piled up work nobody would ever land. Protected lanes
|
|
1282
1372
|
// (money, deploys, security, customer, outward) are never closed by idle
|
|
1283
1373
|
// time. Off switch: policy.idle_sweep === false or policy.janitor === false.
|
|
1284
|
-
if (policy.janitor !== false && policy.idle_sweep !== false) {
|
|
1374
|
+
if (policy.janitor !== false && policy.idle_sweep !== false) timed('idle_sweep', () => {
|
|
1285
1375
|
try {
|
|
1286
1376
|
const liveReasons = new Map(acceptResults
|
|
1287
1377
|
.filter((r) => r && r.ref && r.reason && r.action !== 'accepted')
|
|
@@ -1302,7 +1392,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1302
1392
|
} catch (err) {
|
|
1303
1393
|
receipt.idle_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1304
1394
|
}
|
|
1305
|
-
}
|
|
1395
|
+
});
|
|
1306
1396
|
try {
|
|
1307
1397
|
const { appendOvernightNote, tickSummaryLine } = require('../lib/cleanup-summary');
|
|
1308
1398
|
const line = tickSummaryLine({
|
|
@@ -1321,17 +1411,19 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1321
1411
|
|
|
1322
1412
|
// 3d. wish sweep: queued wishes are allowed to wake a mission without a
|
|
1323
1413
|
// foreground `atris wish` session, but they must never endanger landing.
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1414
|
+
timed('wish_dispatch', () => {
|
|
1415
|
+
try {
|
|
1416
|
+
const wishDispatch = require('./wish').sweepWishes(root);
|
|
1417
|
+
receipt.wish_dispatch = wishDispatch;
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
receipt.wish_dispatch_error = String((err && err.message) || err).slice(0, 200);
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1330
1422
|
|
|
1331
1423
|
// 3e. map fold, once an hour: map notes member changes saved as their own
|
|
1332
1424
|
// files under atris/refs/map-notes.d/ are folded into the shared map notes
|
|
1333
1425
|
// on main and removed, in one pushed commit. Off by policy: map_fold:false.
|
|
1334
|
-
if (policy.map_fold !== false) {
|
|
1426
|
+
if (policy.map_fold !== false) timed('map_fold', () => {
|
|
1335
1427
|
try {
|
|
1336
1428
|
const { foldDue, foldMapNotesOnMain } = require('../lib/member-map-fold');
|
|
1337
1429
|
if (foldDue(state.last_map_fold_at)) {
|
|
@@ -1342,12 +1434,12 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1342
1434
|
} catch (err) {
|
|
1343
1435
|
receipt.map_fold_error = String((err && err.message) || err).slice(0, 200);
|
|
1344
1436
|
}
|
|
1345
|
-
}
|
|
1437
|
+
});
|
|
1346
1438
|
|
|
1347
1439
|
// 4. daily digest at the configured hour
|
|
1348
1440
|
const today = localDay();
|
|
1349
1441
|
const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
|
|
1350
|
-
if (new Date().getHours() === digestHour && !digestSentToday(state)) {
|
|
1442
|
+
if (new Date().getHours() === digestHour && !digestSentToday(state)) timed('daily_digest', () => {
|
|
1351
1443
|
receipt.digest_due = true;
|
|
1352
1444
|
let waitingWishes = [];
|
|
1353
1445
|
try {
|
|
@@ -1385,29 +1477,31 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1385
1477
|
} catch {
|
|
1386
1478
|
receipt.receipts_pruned = null;
|
|
1387
1479
|
}
|
|
1388
|
-
}
|
|
1480
|
+
});
|
|
1389
1481
|
|
|
1390
1482
|
// 5b. daily keep/revert experiment, self-gated inside experiments daily;
|
|
1391
1483
|
// hourly ticks are harmless no-ops after the first run each day.
|
|
1392
1484
|
if (policy.daily_experiment !== false) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1485
|
+
timed('daily_experiment', () => {
|
|
1486
|
+
const daily = runOwnCli(root, ['experiments', 'daily', '--json']);
|
|
1487
|
+
receipt.daily_experiment = {
|
|
1488
|
+
status: daily.status,
|
|
1489
|
+
stdout: daily.stdout.trim(),
|
|
1490
|
+
stderr: daily.stderr.slice(0, 200) || null,
|
|
1491
|
+
};
|
|
1492
|
+
try {
|
|
1493
|
+
receipt.daily_experiment.result = JSON.parse(daily.stdout.trim());
|
|
1494
|
+
} catch {
|
|
1495
|
+
receipt.daily_experiment.result = null;
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1404
1498
|
} else {
|
|
1405
1499
|
receipt.daily_experiment_skipped = true;
|
|
1406
1500
|
}
|
|
1407
1501
|
|
|
1408
1502
|
// 6. once a day, do slower mission cleanup. Landing cleanup runs hourly
|
|
1409
1503
|
// above, outside this date gate, so limbo cannot silently accumulate.
|
|
1410
|
-
if (state.last_mission_cleanup_date !== today) {
|
|
1504
|
+
if (state.last_mission_cleanup_date !== today) timed('mission_cleanup', () => {
|
|
1411
1505
|
// Missions rot the same way branches do: paused/planning/ready and
|
|
1412
1506
|
// untouched for a week means abandoned. Age them out under the same
|
|
1413
1507
|
// daily gate so `mission list` shows work, not archaeology.
|
|
@@ -1435,9 +1529,11 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1435
1529
|
receipt.mission_verify_errors = [{ error: String((err && err.message) || err).slice(0, 200) }];
|
|
1436
1530
|
}
|
|
1437
1531
|
state.last_mission_cleanup_date = today;
|
|
1438
|
-
}
|
|
1532
|
+
});
|
|
1439
1533
|
autoland.writeState(root, state);
|
|
1440
1534
|
|
|
1535
|
+
// Stamped last. The total covers every step and the state write.
|
|
1536
|
+
finish();
|
|
1441
1537
|
try {
|
|
1442
1538
|
persistTickReceipt(root, receipt);
|
|
1443
1539
|
} catch (err) {
|
|
@@ -1458,10 +1554,12 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
1458
1554
|
// Always print the standing backlog, and name work that cleared every
|
|
1459
1555
|
// gate and still did not land. That is the shape of a stuck loop.
|
|
1460
1556
|
const stuckNote = stuckBacklogNote(receipt);
|
|
1461
|
-
const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}${acceptedNotLandedNote(receipt)}${mergedCloseNote(receipt)}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${engineSweepNote}${sshAgentSweepNote(receipt)}${lostRunSweepNote(receipt)}${receiptNote}`;
|
|
1557
|
+
const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}${acceptedNotLandedNote(receipt)}${mergedCloseNote(receipt)}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${engineSweepNote}${sshAgentSweepNote(receipt)}${lostRunSweepNote(receipt)}${tickTimingNote(receipt)}${receiptNote}`;
|
|
1462
1558
|
const validationLine = engineAnswerValidationLine(receipt);
|
|
1463
1559
|
if (validationLine) console.log(validationLine);
|
|
1464
1560
|
console.log(gateForHuman(summary).text);
|
|
1561
|
+
const slowLine = slowStepLine(receipt);
|
|
1562
|
+
if (slowLine) console.log(slowLine);
|
|
1465
1563
|
for (const fulfilled of receipt.wish_dispatch?.fulfilled_results || []) {
|
|
1466
1564
|
if (fulfilled.review_ask) console.log(fulfilled.review_ask);
|
|
1467
1565
|
}
|
|
@@ -1535,6 +1633,10 @@ module.exports = {
|
|
|
1535
1633
|
hasAgentJargon,
|
|
1536
1634
|
runEngineAnswerValidation,
|
|
1537
1635
|
runTickBody,
|
|
1636
|
+
createTickTimer,
|
|
1637
|
+
greenVerdictRow,
|
|
1638
|
+
slowStepLine,
|
|
1639
|
+
tickTimingNote,
|
|
1538
1640
|
stuckBacklogNote,
|
|
1539
1641
|
waitingReasonRows,
|
|
1540
1642
|
acceptCommandArgs,
|