atris 3.35.0 → 3.37.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 +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
package/commands/autoland.js
CHANGED
|
@@ -6,7 +6,9 @@ const os = require('os');
|
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
7
|
|
|
8
8
|
const autoland = require('../lib/autoland');
|
|
9
|
-
const {
|
|
9
|
+
const { gateForHuman } = require('../lib/voice-gate');
|
|
10
|
+
const { evaluateAutoAccept } = require('../lib/auto-accept-certified');
|
|
11
|
+
const { operatorReady, hasAgentJargon, explainResult } = autoland;
|
|
10
12
|
const MISSION_AUTO_VERIFY_STATUSES = new Set(['planning', 'paused', 'ready']);
|
|
11
13
|
const CLOSED_TASK_STATUSES = new Set(['done', 'archived']);
|
|
12
14
|
const MAX_MISSION_AUTO_VERIFY_PER_TICK = 3;
|
|
@@ -21,20 +23,100 @@ function projectName(root) {
|
|
|
21
23
|
return path.basename(root);
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function missionReasonBreakdown(rows, fallback = 'stale') {
|
|
27
|
+
const tally = {};
|
|
28
|
+
for (const row of rows || []) {
|
|
29
|
+
const reason = String(row?.reason || row?.pause_reason || fallback).trim() || fallback;
|
|
30
|
+
tally[reason] = (tally[reason] || 0) + 1;
|
|
31
|
+
}
|
|
32
|
+
return tally;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mergeReasonBreakdown(left = {}, right = {}) {
|
|
36
|
+
const merged = { ...(left && typeof left === 'object' ? left : {}) };
|
|
37
|
+
for (const [reason, count] of Object.entries(right || {})) {
|
|
38
|
+
merged[reason] = (Number(merged[reason]) || 0) + (Number(count) || 0);
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mergeHeldMissions(left = {}, held = []) {
|
|
44
|
+
const merged = { ...(left && typeof left === 'object' ? left : {}) };
|
|
45
|
+
for (const row of held || []) {
|
|
46
|
+
if (!row || !row.id) continue;
|
|
47
|
+
merged[row.id] = {
|
|
48
|
+
id: row.id,
|
|
49
|
+
owner: row.owner || null,
|
|
50
|
+
objective: row.objective || '',
|
|
51
|
+
reason: row.reason || 'operator-required',
|
|
52
|
+
resume_command: row.resume_command || `atris mission run ${row.id}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return merged;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function recordJanitorState(state, { stopped = [], held = [], worktrees = 0 } = {}) {
|
|
59
|
+
const stoppedCount = Array.isArray(stopped) ? stopped.length : Number(stopped) || 0;
|
|
60
|
+
const heldRows = Array.isArray(held) ? held : [];
|
|
61
|
+
const worktreeCount = Number(worktrees) || 0;
|
|
62
|
+
if (!stoppedCount && !heldRows.length && !worktreeCount) return;
|
|
63
|
+
const prior = state.janitor && typeof state.janitor === 'object' ? state.janitor : {};
|
|
64
|
+
const missionHolds = mergeHeldMissions(prior.mission_holds, heldRows);
|
|
65
|
+
const missionStopReasons = mergeReasonBreakdown(prior.mission_stop_reasons, missionReasonBreakdown(stopped, 'stale'));
|
|
66
|
+
state.janitor = {
|
|
67
|
+
...prior,
|
|
68
|
+
missions_stopped: (Number(prior.missions_stopped) || 0) + stoppedCount,
|
|
69
|
+
worktrees_reaped: (Number(prior.worktrees_reaped) || 0) + worktreeCount,
|
|
70
|
+
...(Object.keys(missionStopReasons).length ? { mission_stop_reasons: missionStopReasons } : {}),
|
|
71
|
+
...(Object.keys(missionHolds).length ? { mission_holds: missionHolds, missions_held: Object.keys(missionHolds).length } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
24
75
|
// The digest's "next, if you agree" section: top candidate moves from Atris
|
|
25
76
|
// state, each with the member best suited to own it. Moves that can't explain
|
|
26
77
|
// themselves are counted, not shown. Never blocks the digest.
|
|
78
|
+
function missionHasBudgetRemaining(mission, nowMs = Date.now()) {
|
|
79
|
+
if (mission?.budget_contract?.policy !== 'spend_full_budget') return false;
|
|
80
|
+
const seconds = Number(mission?.budget_contract?.requested_seconds || mission?.max_wall_seconds || 0);
|
|
81
|
+
const startedMs = Date.parse(mission?.started_at || mission?.created_at || mission?.updated_at || '');
|
|
82
|
+
return Number.isFinite(seconds) && seconds > 0
|
|
83
|
+
&& Number.isFinite(startedMs) && startedMs + seconds * 1000 > nowMs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizedMissionObjective(mission) {
|
|
87
|
+
return String(mission?.objective || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
88
|
+
}
|
|
89
|
+
|
|
27
90
|
function digestNextMoves(root) {
|
|
28
91
|
try {
|
|
29
92
|
const { nextMoves } = require('../lib/next-moves');
|
|
30
93
|
const { resolveFunctionalOwner } = require('../lib/functional-owner');
|
|
31
|
-
const
|
|
32
|
-
const
|
|
94
|
+
const { listMissions } = require('./mission');
|
|
95
|
+
const missionRows = listMissions(root) || [];
|
|
96
|
+
const missions = new Map(missionRows.map((mission) => [mission.id, mission]));
|
|
97
|
+
const activeBudgetObjectives = new Set(missionRows
|
|
98
|
+
.filter((mission) => missionHasBudgetRemaining(mission))
|
|
99
|
+
.map(normalizedMissionObjective)
|
|
100
|
+
.filter(Boolean));
|
|
101
|
+
const all = (nextMoves(root, 5) || [])
|
|
102
|
+
.filter((move) => move && move.title)
|
|
103
|
+
.filter((move) => {
|
|
104
|
+
if (move.kind !== 'mission_ready') return true;
|
|
105
|
+
const mission = missions.get(move.ref);
|
|
106
|
+
return !missionHasBudgetRemaining(mission)
|
|
107
|
+
&& !activeBudgetObjectives.has(normalizedMissionObjective(mission));
|
|
108
|
+
});
|
|
109
|
+
const explainable = all.filter((move) => move.kind === 'mission_ready' || operatorReady(move.title));
|
|
110
|
+
const ready = explainable.slice(0, 3).map((move) => {
|
|
33
111
|
let owner = null;
|
|
34
|
-
|
|
35
|
-
|
|
112
|
+
const mission = move.kind === 'mission_ready' ? missions.get(move.ref) : null;
|
|
113
|
+
if (mission?.owner) owner = mission.owner;
|
|
114
|
+
else {
|
|
115
|
+
try { owner = resolveFunctionalOwner({ title: move.title, root })?.owner || null; } catch {}
|
|
116
|
+
}
|
|
117
|
+
return { title: move.title, owner, kind: move.kind || null, label: move.label || null };
|
|
36
118
|
});
|
|
37
|
-
return { moves: ready, unexplained: all.length -
|
|
119
|
+
return { moves: ready, unexplained: all.length - explainable.length };
|
|
38
120
|
} catch {
|
|
39
121
|
return { moves: [], unexplained: 0 };
|
|
40
122
|
}
|
|
@@ -65,6 +147,34 @@ function readProjection(root) {
|
|
|
65
147
|
}
|
|
66
148
|
}
|
|
67
149
|
|
|
150
|
+
function readAcceptedTaskHistory(root, fallbackTasks = [], dbPath = undefined) {
|
|
151
|
+
try {
|
|
152
|
+
const taskDb = require('../lib/task-db');
|
|
153
|
+
const db = taskDb.open(dbPath);
|
|
154
|
+
const rows = taskDb.listTasks(db, { workspaceRoot: root });
|
|
155
|
+
return taskDb.withTaskDisplayRefs(rows);
|
|
156
|
+
} catch {
|
|
157
|
+
return fallbackTasks;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function digestStoryRows(accepted, acceptedTasks) {
|
|
162
|
+
const byRef = new Map((acceptedTasks || []).map((task) => [task.display_id || task.legacy_ref || task.id, task]));
|
|
163
|
+
return (accepted?.auto || []).map((item) => {
|
|
164
|
+
const summary = autoland.digestLine(item);
|
|
165
|
+
if (!summary || !operatorReady(summary)) return null;
|
|
166
|
+
const task = byRef.get(item.ref);
|
|
167
|
+
const candidates = [
|
|
168
|
+
task?.review?.landing?.happened || task?.metadata?.landing_happened || '',
|
|
169
|
+
item.result,
|
|
170
|
+
item.happened,
|
|
171
|
+
autoland.clarify(item.title, 160),
|
|
172
|
+
summary,
|
|
173
|
+
].map((candidate) => autoland.historicalLandingText(candidate, 160));
|
|
174
|
+
return { item, story: candidates.find((candidate) => operatorReady(candidate)) || summary };
|
|
175
|
+
}).filter(Boolean);
|
|
176
|
+
}
|
|
177
|
+
|
|
68
178
|
function compactId(value) {
|
|
69
179
|
return String(value || '').trim();
|
|
70
180
|
}
|
|
@@ -197,28 +307,143 @@ function landSummarySafe(root) {
|
|
|
197
307
|
}
|
|
198
308
|
}
|
|
199
309
|
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
310
|
+
function refreshLandingRefs(root) {
|
|
311
|
+
const remotes = spawnSync('git', ['remote'], { cwd: root, encoding: 'utf8', timeout: 10000 });
|
|
312
|
+
if (remotes.status !== 0 || !remotes.stdout.split(/\r?\n/).includes('origin')) return;
|
|
313
|
+
// One round-trip for both; a missing branch doesn't fail the other with a
|
|
314
|
+
// multi-ref fetch on any git that supports it, and fetch errors are ignored
|
|
315
|
+
// here anyway.
|
|
316
|
+
const both = spawnSync('git', ['fetch', 'origin', 'master', 'main'], { cwd: root, encoding: 'utf8', timeout: 30000 });
|
|
317
|
+
if (both.status === 0) return;
|
|
318
|
+
for (const branch of ['master', 'main']) {
|
|
319
|
+
spawnSync('git', ['fetch', 'origin', branch], { cwd: root, encoding: 'utf8', timeout: 30000 });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function sweepLanding(root, { ttlDays, staleHours, now = Date.now() } = {}) {
|
|
324
|
+
refreshLandingRefs(root);
|
|
325
|
+
const { collectBoard, reap } = require('./land');
|
|
326
|
+
// light: this board only feeds .branches staleness + .summary; reap builds
|
|
327
|
+
// its own full board for dirty-count salvage decisions.
|
|
328
|
+
const beforeBoard = collectBoard(root, { ttlDays, staleHours, now, light: true });
|
|
329
|
+
const stale = beforeBoard.branches
|
|
330
|
+
.filter((b) => b.state === 'active' && b.stale)
|
|
331
|
+
.sort((a, b) => b.activityHours - a.activityHours)
|
|
332
|
+
.map((b) => ({
|
|
333
|
+
name: b.name,
|
|
334
|
+
ahead: b.ahead,
|
|
335
|
+
ageHours: b.ageHours,
|
|
336
|
+
activityHours: b.activityHours,
|
|
337
|
+
}));
|
|
338
|
+
const reaped = reap(root, {
|
|
339
|
+
ttlDays,
|
|
340
|
+
staleHours,
|
|
341
|
+
remote: false,
|
|
342
|
+
includeDetached: false,
|
|
343
|
+
now,
|
|
344
|
+
});
|
|
345
|
+
const afterBoard = collectBoard(root, { ttlDays, staleHours, now, light: true });
|
|
346
|
+
const human = [];
|
|
347
|
+
for (const kept of reaped.keptWorktrees || []) {
|
|
348
|
+
if (String(kept).includes('(fresh_worktree_grace)')) continue;
|
|
349
|
+
if (String(kept).includes('(current_checkout)')) continue;
|
|
350
|
+
human.push(String(kept));
|
|
351
|
+
}
|
|
352
|
+
for (const moved of reaped.keptMovedBranches || []) human.push(`${moved} moved during cleanup`);
|
|
353
|
+
if (reaped.bundleError) human.push(`backup failed: ${reaped.bundleError}`);
|
|
354
|
+
return {
|
|
355
|
+
at: new Date(now).toISOString(),
|
|
356
|
+
before: beforeBoard.summary,
|
|
357
|
+
after: afterBoard.summary,
|
|
358
|
+
stale,
|
|
359
|
+
human,
|
|
360
|
+
reaped: {
|
|
361
|
+
branches: reaped.deletedBranches.length,
|
|
362
|
+
worktrees: reaped.removedWorktrees.length,
|
|
363
|
+
bundle: reaped.bundle,
|
|
364
|
+
patches: reaped.patches.length,
|
|
365
|
+
untracked: (reaped.untracked || []).length,
|
|
366
|
+
bundleError: reaped.bundleError || null,
|
|
367
|
+
keptWorktrees: reaped.keptWorktrees || [],
|
|
368
|
+
keptMovedBranches: reaped.keptMovedBranches || [],
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function recordLandingSweepState(state, sweep, { error = null } = {}) {
|
|
374
|
+
const at = sweep?.at || new Date().toISOString();
|
|
375
|
+
const today = at.slice(0, 10);
|
|
376
|
+
state.last_reap_date = today;
|
|
377
|
+
state.last_reap_at = at;
|
|
378
|
+
if (error) {
|
|
379
|
+
state.last_reap_error = { date: today, error };
|
|
380
|
+
delete state.landing_sweep;
|
|
381
|
+
return;
|
|
210
382
|
}
|
|
383
|
+
delete state.last_reap_error;
|
|
384
|
+
state.landing_sweep = {
|
|
385
|
+
at,
|
|
386
|
+
before: sweep.before,
|
|
387
|
+
after: sweep.after,
|
|
388
|
+
reaped: sweep.reaped,
|
|
389
|
+
stale_count: sweep.stale.length,
|
|
390
|
+
stale: sweep.stale.slice(0, 10),
|
|
391
|
+
human_count: sweep.human.length,
|
|
392
|
+
human: sweep.human.slice(0, 10),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function wishSweepSummaryLine(summary, error = null) {
|
|
397
|
+
if (error) return `wishes: sweep failed (${error})`;
|
|
398
|
+
const dispatched = Number(summary?.dispatched) || 0;
|
|
399
|
+
const fulfilled = Number(summary?.fulfilled) || 0;
|
|
400
|
+
const waiting = Number(summary?.waiting_on_operator) || 0;
|
|
401
|
+
const noExecutor = Number(summary?.skipped_no_executor) || 0;
|
|
402
|
+
const capped = Number(summary?.capped) || 0;
|
|
403
|
+
let line = `wishes: ${dispatched} dispatched, ${waiting} waiting on operator, ${fulfilled} fulfilled`;
|
|
404
|
+
const notes = [];
|
|
405
|
+
if (noExecutor > 0) notes.push(`${noExecutor} need a working builder`);
|
|
406
|
+
if (capped > 0) notes.push(`${capped} held for the next tick`);
|
|
407
|
+
if (notes.length) line += ` (${notes.join(', ')})`;
|
|
408
|
+
return line;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function evaluateQueue(root, { strictVerify, acceptAll }) {
|
|
412
|
+
return readProjection(root)
|
|
413
|
+
.filter((task) => task && task.status === 'review')
|
|
414
|
+
.filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
|
|
415
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
|
|
416
|
+
.slice(0, 50)
|
|
417
|
+
.map((task) => {
|
|
418
|
+
const evaluation = evaluateAutoAccept(
|
|
419
|
+
{ ...task, workspace_root: root },
|
|
420
|
+
{ strictVerify, acceptAll, executeVerify: false },
|
|
421
|
+
);
|
|
422
|
+
return {
|
|
423
|
+
...evaluation,
|
|
424
|
+
action: evaluation.eligible ? 'would_accept' : 'skipped',
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function protectedReviewWaiting(root, policy = {}, waiting = []) {
|
|
430
|
+
const alreadyWaiting = new Set((waiting || []).map((row) => row.ref));
|
|
431
|
+
return evaluateQueue(root, {
|
|
432
|
+
strictVerify: policy.strict_verify !== false,
|
|
433
|
+
acceptAll: Boolean(policy.accept_all),
|
|
434
|
+
})
|
|
435
|
+
.filter((row) => /^denied_tag_/.test(String(row.reason || '')))
|
|
436
|
+
.filter((row) => !alreadyWaiting.has(row.ref));
|
|
211
437
|
}
|
|
212
438
|
|
|
213
439
|
function plainReason(reason) {
|
|
214
440
|
const map = {
|
|
215
|
-
denied_tag_billing: 'money
|
|
216
|
-
denied_tag_deploy: 'a deploy
|
|
217
|
-
denied_tag_security: 'security
|
|
218
|
-
denied_tag_customer: 'customer-facing
|
|
219
|
-
denied_tag_external: 'outward-facing
|
|
220
|
-
denied_tag_feedback: 'customer feedback
|
|
221
|
-
denied_tag_voice: 'voice/comms — yours to approve',
|
|
441
|
+
denied_tag_billing: 'money: human decision required',
|
|
442
|
+
denied_tag_deploy: 'a deploy: human decision required',
|
|
443
|
+
denied_tag_security: 'security: human decision required',
|
|
444
|
+
denied_tag_customer: 'customer-facing: human decision required',
|
|
445
|
+
denied_tag_external: 'outward-facing: human decision required',
|
|
446
|
+
denied_tag_feedback: 'customer feedback: human decision required',
|
|
222
447
|
needs_second_reviewer_or_third_pass: 'needs one more independent check first',
|
|
223
448
|
needs_independent_reviewer: 'built and judged by the same actor, needs an independent check',
|
|
224
449
|
verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
|
|
@@ -240,17 +465,22 @@ function showStatus(root, args) {
|
|
|
240
465
|
const acceptAll = Boolean(policy && policy.accept_all);
|
|
241
466
|
const results = evaluateQueue(root, { strictVerify, acceptAll })
|
|
242
467
|
.filter((r) => r.reason !== 'not_in_review');
|
|
243
|
-
const
|
|
468
|
+
const readyForRecheck = results.filter((r) => r.action === 'would_accept');
|
|
469
|
+
const wouldLand = readyForRecheck.filter((r) => r.verification_pending !== true);
|
|
244
470
|
const blocked = results.filter((r) => r.action !== 'would_accept');
|
|
245
471
|
const tasks = readProjection(root);
|
|
246
472
|
const waiting = autoland.waitingOnHuman(tasks);
|
|
473
|
+
const heartbeatInstalled = typeof policy?.heartbeat_installed === 'boolean'
|
|
474
|
+
? policy.heartbeat_installed
|
|
475
|
+
: null;
|
|
247
476
|
|
|
248
477
|
if (json) {
|
|
249
478
|
console.log(JSON.stringify({
|
|
250
479
|
enabled,
|
|
251
480
|
policy,
|
|
252
|
-
heartbeat_installed:
|
|
481
|
+
heartbeat_installed: heartbeatInstalled,
|
|
253
482
|
would_land: wouldLand,
|
|
483
|
+
ready_for_recheck: readyForRecheck,
|
|
254
484
|
blocked,
|
|
255
485
|
waiting_on_human: waiting,
|
|
256
486
|
}, null, 2));
|
|
@@ -258,7 +488,7 @@ function showStatus(root, args) {
|
|
|
258
488
|
}
|
|
259
489
|
|
|
260
490
|
console.log('');
|
|
261
|
-
console.log(
|
|
491
|
+
console.log('autoland: certified work lands itself; you keep the irreversible calls');
|
|
262
492
|
console.log('');
|
|
263
493
|
const policyOwner = String(policy?.enabled_by || 'unknown').trim() || 'unknown';
|
|
264
494
|
const policyText = enabled
|
|
@@ -268,26 +498,31 @@ function showStatus(root, args) {
|
|
|
268
498
|
: 'off - everything waits for you';
|
|
269
499
|
console.log(` policy: ${policyText}`);
|
|
270
500
|
if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
|
|
271
|
-
|
|
501
|
+
const heartbeatText = heartbeatInstalled === true
|
|
502
|
+
? 'running hourly'
|
|
503
|
+
: heartbeatInstalled === false
|
|
504
|
+
? 'not installed'
|
|
505
|
+
: 'unknown - run atris autoland on to check and repair';
|
|
506
|
+
console.log(` heartbeat: ${heartbeatText}`);
|
|
272
507
|
if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
|
|
273
508
|
const reapTrouble = autoland.readState(root).last_reap_error;
|
|
274
|
-
if (reapTrouble) console.log(` cleanup trouble:
|
|
509
|
+
if (reapTrouble) console.log(` cleanup trouble: landing sweep failed on ${reapTrouble.date} (${reapTrouble.error}) - run: atris land --reap`);
|
|
275
510
|
console.log('');
|
|
276
|
-
if (
|
|
277
|
-
console.log(` ready
|
|
278
|
-
for (const r of
|
|
511
|
+
if (readyForRecheck.length > 0) {
|
|
512
|
+
console.log(` ready for heartbeat recheck: ${readyForRecheck.length}`);
|
|
513
|
+
for (const r of readyForRecheck.slice(0, 10)) console.log(` rechecks then lands ${r.ref}`);
|
|
279
514
|
} else {
|
|
280
515
|
console.log(' nothing is ready to land on its own right now.');
|
|
281
516
|
}
|
|
282
517
|
const humanOnly = blocked.filter((r) => String(r.reason || '').startsWith('denied_tag_'));
|
|
283
518
|
const needsWork = blocked.filter((r) => !String(r.reason || '').startsWith('denied_tag_'));
|
|
284
519
|
if (humanOnly.length > 0) {
|
|
285
|
-
console.log(`
|
|
286
|
-
for (const r of humanOnly.slice(0, 10)) console.log(` waits for you ${r.ref}
|
|
520
|
+
console.log(` protected reviews waiting on you: ${humanOnly.length}`);
|
|
521
|
+
for (const r of humanOnly.slice(0, 10)) console.log(` waits for you ${r.ref} - ${plainReason(r.reason)}`);
|
|
287
522
|
}
|
|
288
523
|
if (needsWork.length > 0) {
|
|
289
524
|
console.log(` not ready yet: ${needsWork.length}`);
|
|
290
|
-
for (const r of needsWork.slice(0, 10)) console.log(` held back ${r.ref}
|
|
525
|
+
for (const r of needsWork.slice(0, 10)) console.log(` held back ${r.ref} - ${plainReason(String(r.reason || ''))}`);
|
|
291
526
|
}
|
|
292
527
|
if (waiting.length > 0) {
|
|
293
528
|
console.log('');
|
|
@@ -305,6 +540,7 @@ function turnOn(root, args) {
|
|
|
305
540
|
const digestHour = Number(flag(args, '--digest-hour', autoland.DEFAULT_DIGEST_HOUR));
|
|
306
541
|
const alarmHours = Number(flag(args, '--alarm-hours', autoland.DEFAULT_ALARM_HOURS));
|
|
307
542
|
const acceptAll = args.includes('--all');
|
|
543
|
+
const previous = autoland.readPolicy(root);
|
|
308
544
|
const policy = autoland.writePolicy(root, {
|
|
309
545
|
enabled: true,
|
|
310
546
|
enabled_by: owner,
|
|
@@ -314,18 +550,24 @@ function turnOn(root, args) {
|
|
|
314
550
|
alarm_hours: Number.isFinite(alarmHours) && alarmHours > 0 ? alarmHours : autoland.DEFAULT_ALARM_HOURS,
|
|
315
551
|
strict_verify: !args.includes('--no-strict-verify'),
|
|
316
552
|
accept_all: acceptAll,
|
|
553
|
+
daily_experiment: previous.daily_experiment !== false,
|
|
317
554
|
});
|
|
318
555
|
const cronOk = autoland.installCron(root);
|
|
556
|
+
autoland.writePolicy(root, {
|
|
557
|
+
...policy,
|
|
558
|
+
heartbeat_installed: cronOk,
|
|
559
|
+
heartbeat_checked_at: new Date().toISOString(),
|
|
560
|
+
});
|
|
319
561
|
console.log('');
|
|
320
562
|
console.log('autoland is on.');
|
|
321
|
-
if (acceptAll) console.log(` everything in review now lands itself, accepted as ${owner}
|
|
563
|
+
if (acceptAll) console.log(` everything in review now lands itself, accepted as ${owner}; only the protected lanes wait.`);
|
|
322
564
|
else console.log(` certified, verified, reversible work now lands itself, accepted as ${owner}.`);
|
|
323
565
|
console.log(' protected lanes (money, deploys, security, customer, outward) still wait for you.');
|
|
324
|
-
console.log(` heartbeat: ${cronOk ? 'installed, runs hourly' : 'could not install cron
|
|
566
|
+
console.log(` heartbeat: ${cronOk ? 'installed, runs hourly' : 'could not install cron; run atris autoland tick yourself'}`);
|
|
325
567
|
if (policy.imessage_to) {
|
|
326
568
|
console.log(` daily message to ${policy.imessage_to} at ${policy.digest_hour}:00; anything waiting on you past ${policy.alarm_hours}h pings you.`);
|
|
327
569
|
} else {
|
|
328
|
-
console.log(' no phone number set
|
|
570
|
+
console.log(' no phone number set; digest goes to the log only. add one: atris autoland on --to <your number>');
|
|
329
571
|
}
|
|
330
572
|
console.log(' turn it off any time: atris autoland off');
|
|
331
573
|
console.log('');
|
|
@@ -334,11 +576,20 @@ function turnOn(root, args) {
|
|
|
334
576
|
|
|
335
577
|
function turnOff(root) {
|
|
336
578
|
const policy = autoland.readPolicy(root) || {};
|
|
337
|
-
autoland.writePolicy(root, {
|
|
579
|
+
const disabledPolicy = autoland.writePolicy(root, {
|
|
580
|
+
...policy,
|
|
581
|
+
enabled: false,
|
|
582
|
+
disabled_at: new Date().toISOString(),
|
|
583
|
+
});
|
|
338
584
|
const cronOk = autoland.uninstallCron(root);
|
|
585
|
+
autoland.writePolicy(root, {
|
|
586
|
+
...disabledPolicy,
|
|
587
|
+
heartbeat_installed: cronOk ? false : null,
|
|
588
|
+
heartbeat_checked_at: new Date().toISOString(),
|
|
589
|
+
});
|
|
339
590
|
console.log('');
|
|
340
591
|
console.log('autoland is off. everything waits for your accept again.');
|
|
341
|
-
console.log(` heartbeat ${cronOk ? 'removed' : 'removal failed
|
|
592
|
+
console.log(` heartbeat ${cronOk ? 'removed' : 'removal failed; check crontab -l'}.`);
|
|
342
593
|
console.log('');
|
|
343
594
|
return 0;
|
|
344
595
|
}
|
|
@@ -346,34 +597,39 @@ function turnOff(root) {
|
|
|
346
597
|
function runDigest(root, args, { forceSend = false } = {}) {
|
|
347
598
|
const policy = autoland.readPolicy(root) || {};
|
|
348
599
|
const tasks = readProjection(root);
|
|
349
|
-
const
|
|
600
|
+
const acceptedTasks = readAcceptedTaskHistory(root, tasks);
|
|
601
|
+
const accepted = autoland.acceptedInLastDay(acceptedTasks);
|
|
602
|
+
const state = autoland.readState(root);
|
|
603
|
+
let waitingWishes = [];
|
|
604
|
+
try {
|
|
605
|
+
waitingWishes = require('./wish').waitingOperatorWishes(root);
|
|
606
|
+
} catch {}
|
|
607
|
+
const waiting = autoland.waitingOnHuman(tasks);
|
|
350
608
|
const text = autoland.composeDigest({
|
|
351
609
|
accepted,
|
|
352
|
-
waiting
|
|
610
|
+
waiting,
|
|
611
|
+
protectedWaiting: protectedReviewWaiting(root, policy, waiting),
|
|
612
|
+
waitingWishes,
|
|
353
613
|
landed: landSummarySafe(root),
|
|
354
614
|
project: projectName(root),
|
|
355
615
|
nextMoves: digestNextMoves(root),
|
|
356
616
|
acceptAll: Boolean(policy.accept_all),
|
|
357
|
-
reapError:
|
|
617
|
+
reapError: state.last_reap_error?.error || null,
|
|
618
|
+
landingSweep: state.landing_sweep || null,
|
|
619
|
+
fullStory: true,
|
|
620
|
+
root,
|
|
358
621
|
});
|
|
359
622
|
console.log(text);
|
|
360
623
|
// the full story: what each piece actually was, in its own words
|
|
361
|
-
const
|
|
362
|
-
const storied = accepted.auto.filter((item) => {
|
|
363
|
-
const t = byRef.get(item.ref);
|
|
364
|
-
return t && (t.review?.landing?.happened || t.metadata?.landing_happened);
|
|
365
|
-
});
|
|
624
|
+
const storied = digestStoryRows(accepted, acceptedTasks).slice(3);
|
|
366
625
|
if (storied.length > 0) {
|
|
367
626
|
let printedStoryHeader = false;
|
|
368
|
-
for (const item of storied) {
|
|
369
|
-
const t = byRef.get(item.ref);
|
|
370
|
-
const happened = String(t.review?.landing?.happened || t.metadata?.landing_happened || '').replace(/\s+/g, ' ').slice(0, 160);
|
|
371
|
-
if (!operatorReady(happened)) continue;
|
|
627
|
+
for (const { item, story } of storied) {
|
|
372
628
|
if (!printedStoryHeader) {
|
|
373
629
|
console.log('');
|
|
374
630
|
printedStoryHeader = true;
|
|
375
631
|
}
|
|
376
|
-
console.log(` ${
|
|
632
|
+
console.log(` - ${story}`);
|
|
377
633
|
}
|
|
378
634
|
}
|
|
379
635
|
const shouldSend = (forceSend || args.includes('--send')) && policy.imessage_to;
|
|
@@ -397,7 +653,7 @@ function pidAlive(pid) {
|
|
|
397
653
|
function runTick(root, args) {
|
|
398
654
|
const json = args.includes('--json');
|
|
399
655
|
const policy = autoland.readPolicy(root);
|
|
400
|
-
const receipt = { at: new Date().toISOString(), landed: [], alarms: 0, digest_sent: false, enabled: Boolean(policy && policy.enabled) };
|
|
656
|
+
const receipt = { at: new Date().toISOString(), landed: [], alarms: 0, digest_due: false, digest_sent: false, enabled: Boolean(policy && policy.enabled) };
|
|
401
657
|
if (!policy || policy.enabled !== true) {
|
|
402
658
|
if (json) console.log(JSON.stringify(receipt));
|
|
403
659
|
else console.log('autoland is off; tick did nothing.');
|
|
@@ -430,42 +686,57 @@ function runTick(root, args) {
|
|
|
430
686
|
}
|
|
431
687
|
}
|
|
432
688
|
|
|
433
|
-
function
|
|
689
|
+
function persistTickReceipt(root, receipt) {
|
|
690
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
691
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
692
|
+
const safeTime = String(receipt.at || new Date().toISOString()).replace(/[:.]/g, '-');
|
|
693
|
+
const receiptPath = path.join(runsDir, `autoland-tick-${safeTime}.json`);
|
|
694
|
+
receipt.receipt_path = path.relative(root, receiptPath);
|
|
695
|
+
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
696
|
+
return receipt.receipt_path;
|
|
697
|
+
}
|
|
434
698
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
receipt.reviews_certified = parsed.certified ?? 0;
|
|
443
|
-
if (parsed.ok !== true) receipt.certify_error = 'certify-verified failed';
|
|
444
|
-
} catch {
|
|
445
|
-
receipt.certify_error = certify.stderr.slice(0, 200) || 'certify-verified output unreadable';
|
|
446
|
-
}
|
|
699
|
+
function digestTickStatus(receipt) {
|
|
700
|
+
if (receipt.digest_sent) return 'sent';
|
|
701
|
+
if (receipt.digest_due) return 'saved';
|
|
702
|
+
return 'not due';
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function runTickBody(root, { json, policy, receipt }) {
|
|
447
706
|
|
|
448
|
-
//
|
|
707
|
+
// 1. certify and land in one task process. Keeping both phases together lets
|
|
708
|
+
// the landing gate reuse the live certification verifier result without
|
|
709
|
+
// persisting trust across heartbeats. Denied lanes and check-less proofs wait.
|
|
449
710
|
// No hardcoded --limit here: a fixed low cap (this used to be 12) silently
|
|
450
711
|
// undercounts a real backlog every single tick — 12/hour forever even with
|
|
451
712
|
// 78 certified rows waiting. Let `atris task auto-accept-certified` apply
|
|
452
713
|
// its own default (12 without --all, a high safety cap under --all) so a
|
|
453
714
|
// policy with accept_all:true actually drains the full certified set.
|
|
454
|
-
const cliArgs = ['task', 'auto-accept-certified', '--json'];
|
|
715
|
+
const cliArgs = ['task', 'auto-accept-certified', '--json', '--certify-first'];
|
|
455
716
|
if (policy.accept_all) cliArgs.push('--all');
|
|
456
717
|
else if (policy.strict_verify === false) cliArgs.push('--no-strict-verify');
|
|
457
718
|
const accept = runOwnCli(root, cliArgs);
|
|
458
719
|
try {
|
|
459
720
|
const parsed = JSON.parse(accept.stdout);
|
|
721
|
+
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
460
722
|
// A refused sweep (ok:false — a guard tripped, policy race) carries no
|
|
461
723
|
// summary fields. Name the reason instead of leaving nulls that read as
|
|
462
724
|
// "no work": a blind heartbeat must say WHY it is blind.
|
|
463
725
|
if (parsed.ok === false) {
|
|
464
726
|
receipt.accept_error = String(parsed.reason || 'auto-accept refused');
|
|
465
727
|
}
|
|
466
|
-
receipt.
|
|
728
|
+
receipt.reviews_certified = parsed.certification?.certified ?? 0;
|
|
729
|
+
if (parsed.certification && parsed.certification.ok !== true) {
|
|
730
|
+
receipt.certify_error = 'certify-verified failed';
|
|
731
|
+
}
|
|
732
|
+
receipt.landed = results.filter((r) => r.action === 'accepted').map((r) => r.ref);
|
|
733
|
+
const citationBlocks = results
|
|
734
|
+
.filter((r) => r.action === 'skipped' && r.proof_state === 'suite_green_citation_required')
|
|
735
|
+
.map((r) => ({ ref: r.ref, reason: r.reason }));
|
|
736
|
+
if (citationBlocks.length) receipt.citation_blocks = citationBlocks;
|
|
467
737
|
receipt.certified = parsed.certified ?? null;
|
|
468
738
|
receipt.scanned = parsed.scanned ?? null;
|
|
739
|
+
receipt.revised = parsed.revised ?? null;
|
|
469
740
|
receipt.skipped = parsed.skipped ?? null;
|
|
470
741
|
receipt.undercounted = Boolean(parsed.undercounted);
|
|
471
742
|
} catch (err) {
|
|
@@ -481,8 +752,12 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
481
752
|
landedRefs: receipt.landed,
|
|
482
753
|
tasks: tasksForLive,
|
|
483
754
|
project: projectName(root),
|
|
755
|
+
root,
|
|
484
756
|
});
|
|
485
757
|
if (text) {
|
|
758
|
+
// Keep the exact operator surface beside the delivery result. The next
|
|
759
|
+
// linguist pass must audit what was sent, not reconstruct today's code.
|
|
760
|
+
receipt.live_update_text = text;
|
|
486
761
|
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
487
762
|
receipt.live_update_sent = sent.ok;
|
|
488
763
|
}
|
|
@@ -492,6 +767,7 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
492
767
|
const state = autoland.readState(root);
|
|
493
768
|
const tasks = readProjection(root);
|
|
494
769
|
const waiting = autoland.waitingOnHuman(tasks);
|
|
770
|
+
const protectedWaiting = protectedReviewWaiting(root, policy, waiting);
|
|
495
771
|
const alarmHours = Number(policy.alarm_hours) || autoland.DEFAULT_ALARM_HOURS;
|
|
496
772
|
const due = autoland.dueForAlarm(waiting, state, { alarmHours });
|
|
497
773
|
if (due.length > 0 && policy.imessage_to) {
|
|
@@ -512,13 +788,23 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
512
788
|
// accumulate in state so the daily digest reports the day's total, not
|
|
513
789
|
// just the last tick's.
|
|
514
790
|
receipt.missions_stopped = 0;
|
|
791
|
+
receipt.missions_held = 0;
|
|
515
792
|
receipt.worktrees_reaped = 0;
|
|
516
793
|
if (policy.janitor !== false) {
|
|
794
|
+
let stoppedMissions = [];
|
|
795
|
+
let heldMissions = [];
|
|
517
796
|
try {
|
|
518
797
|
const { reapPausedMissions } = require('./mission');
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
798
|
+
stoppedMissions = reapPausedMissions(root);
|
|
799
|
+
heldMissions = Array.isArray(stoppedMissions.held) ? stoppedMissions.held : [];
|
|
800
|
+
receipt.missions_stopped = stoppedMissions.length;
|
|
801
|
+
if (stoppedMissions.length > 0) receipt.missions_stopped_refs = stoppedMissions.map((m) => m.id);
|
|
802
|
+
if (stoppedMissions.length > 0) receipt.missions_stopped_reasons = missionReasonBreakdown(stoppedMissions, 'stale');
|
|
803
|
+
receipt.missions_held = heldMissions.length;
|
|
804
|
+
if (heldMissions.length > 0) {
|
|
805
|
+
receipt.missions_held_refs = heldMissions.map((m) => m.id);
|
|
806
|
+
receipt.missions_held_reasons = missionReasonBreakdown(heldMissions, 'operator-required');
|
|
807
|
+
}
|
|
522
808
|
} catch (err) {
|
|
523
809
|
receipt.janitor_mission_error = String((err && err.message) || err).slice(0, 200);
|
|
524
810
|
}
|
|
@@ -530,28 +816,61 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
530
816
|
} catch (err) {
|
|
531
817
|
receipt.janitor_worktree_error = String((err && err.message) || err).slice(0, 200);
|
|
532
818
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
819
|
+
recordJanitorState(state, { stopped: stoppedMissions, held: heldMissions, worktrees: receipt.worktrees_reaped });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// 3c. landing sweep, every tick: merged branch residue is cleared,
|
|
823
|
+
// TTL-expired work is salvaged before deletion, and 48h-stale active work
|
|
824
|
+
// is carried into the digest instead of silently aging forever.
|
|
825
|
+
try {
|
|
826
|
+
const landingSweep = sweepLanding(root);
|
|
827
|
+
receipt.reaped = landingSweep.reaped;
|
|
828
|
+
receipt.landing_sweep = {
|
|
829
|
+
before: landingSweep.before,
|
|
830
|
+
after: landingSweep.after,
|
|
831
|
+
stale: landingSweep.stale.length,
|
|
832
|
+
human: landingSweep.human.length,
|
|
833
|
+
};
|
|
834
|
+
if (landingSweep.reaped.bundleError) {
|
|
835
|
+
receipt.reap_error = `backup failed, unlanded work kept in place: ${landingSweep.reaped.bundleError}`;
|
|
539
836
|
}
|
|
837
|
+
recordLandingSweepState(state, landingSweep);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
receipt.reap_error = String((err && err.message) || err).slice(0, 200);
|
|
840
|
+
recordLandingSweepState(state, null, { error: receipt.reap_error });
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// 3d. wish sweep: queued wishes are allowed to wake a mission without a
|
|
844
|
+
// foreground `atris wish` session, but they must never endanger landing.
|
|
845
|
+
try {
|
|
846
|
+
const wishDispatch = require('./wish').sweepWishes(root);
|
|
847
|
+
receipt.wish_dispatch = wishDispatch;
|
|
848
|
+
} catch (err) {
|
|
849
|
+
receipt.wish_dispatch_error = String((err && err.message) || err).slice(0, 200);
|
|
540
850
|
}
|
|
541
851
|
|
|
542
852
|
// 4. daily digest at the configured hour
|
|
543
853
|
const today = new Date().toISOString().slice(0, 10);
|
|
544
854
|
const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
|
|
545
855
|
if (new Date().getHours() === digestHour && state.last_digest_date !== today) {
|
|
856
|
+
receipt.digest_due = true;
|
|
857
|
+
let waitingWishes = [];
|
|
858
|
+
try {
|
|
859
|
+
waitingWishes = require('./wish').waitingOperatorWishes(root);
|
|
860
|
+
} catch {}
|
|
546
861
|
const text = autoland.composeDigest({
|
|
547
|
-
accepted: autoland.acceptedInLastDay(tasks),
|
|
862
|
+
accepted: autoland.acceptedInLastDay(readAcceptedTaskHistory(root, tasks)),
|
|
548
863
|
waiting,
|
|
864
|
+
protectedWaiting,
|
|
865
|
+
waitingWishes,
|
|
549
866
|
landed: landSummarySafe(root),
|
|
550
867
|
project: projectName(root),
|
|
551
868
|
nextMoves: digestNextMoves(root),
|
|
552
869
|
acceptAll: Boolean(policy.accept_all),
|
|
553
870
|
reapError: state.last_reap_error?.error || null,
|
|
554
871
|
janitor: state.janitor || null,
|
|
872
|
+
landingSweep: state.landing_sweep || null,
|
|
873
|
+
root,
|
|
555
874
|
});
|
|
556
875
|
if (policy.imessage_to) {
|
|
557
876
|
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
@@ -571,34 +890,44 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
571
890
|
receipt.receipts_pruned = null;
|
|
572
891
|
}
|
|
573
892
|
}
|
|
574
|
-
|
|
575
|
-
// .
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
893
|
+
|
|
894
|
+
// 5b. daily keep/revert experiment — self-gated inside experiments daily;
|
|
895
|
+
// hourly ticks are harmless no-ops after the first run each day.
|
|
896
|
+
if (policy.daily_experiment !== false) {
|
|
897
|
+
const daily = runOwnCli(root, ['experiments', 'daily', '--json']);
|
|
898
|
+
receipt.daily_experiment = {
|
|
899
|
+
status: daily.status,
|
|
900
|
+
stdout: daily.stdout.trim(),
|
|
901
|
+
stderr: daily.stderr.slice(0, 200) || null,
|
|
902
|
+
};
|
|
582
903
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
receipt.
|
|
586
|
-
branches: reaped.deletedBranches.length,
|
|
587
|
-
worktrees: reaped.removedWorktrees.length,
|
|
588
|
-
bundle: reaped.bundle,
|
|
589
|
-
patches: reaped.patches.length,
|
|
590
|
-
};
|
|
591
|
-
if (reaped.bundleError) receipt.reap_error = `backup failed, unlanded work kept in place: ${reaped.bundleError}`;
|
|
592
|
-
} catch (err) {
|
|
593
|
-
receipt.reap_error = String((err && err.message) || err).slice(0, 200);
|
|
904
|
+
receipt.daily_experiment.result = JSON.parse(daily.stdout.trim());
|
|
905
|
+
} catch {
|
|
906
|
+
receipt.daily_experiment.result = null;
|
|
594
907
|
}
|
|
908
|
+
} else {
|
|
909
|
+
receipt.daily_experiment_skipped = true;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 6. once a day, do slower mission cleanup. Landing cleanup runs hourly
|
|
913
|
+
// above, outside this date gate, so limbo cannot silently accumulate.
|
|
914
|
+
if (state.last_mission_cleanup_date !== today) {
|
|
595
915
|
// Missions rot the same way branches do: paused/planning/ready and
|
|
596
916
|
// untouched for a week means abandoned. Age them out under the same
|
|
597
917
|
// daily gate so `mission list` shows work, not archaeology.
|
|
598
918
|
try {
|
|
599
919
|
const { expireStaleMissions } = require('./mission');
|
|
600
920
|
const expiredMissions = expireStaleMissions(root);
|
|
921
|
+
const heldMissions = Array.isArray(expiredMissions.held) ? expiredMissions.held : [];
|
|
601
922
|
if (expiredMissions.length > 0) receipt.expired_missions = expiredMissions.length;
|
|
923
|
+
if (expiredMissions.length > 0) receipt.expired_mission_reasons = missionReasonBreakdown(expiredMissions, 'stale');
|
|
924
|
+
if (expiredMissions.length > 0) recordJanitorState(state, { stopped: expiredMissions });
|
|
925
|
+
if (heldMissions.length > 0) {
|
|
926
|
+
receipt.expired_missions_held = heldMissions.length;
|
|
927
|
+
receipt.expired_missions_held_refs = heldMissions.map((m) => m.id);
|
|
928
|
+
receipt.expired_missions_held_reasons = missionReasonBreakdown(heldMissions, 'operator-required');
|
|
929
|
+
recordJanitorState(state, { held: heldMissions });
|
|
930
|
+
}
|
|
602
931
|
} catch (err) {
|
|
603
932
|
receipt.mission_expiry_error = String((err && err.message) || err).slice(0, 200);
|
|
604
933
|
}
|
|
@@ -609,32 +938,46 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
609
938
|
} catch (err) {
|
|
610
939
|
receipt.mission_verify_errors = [{ error: String((err && err.message) || err).slice(0, 200) }];
|
|
611
940
|
}
|
|
612
|
-
state.
|
|
613
|
-
// a failed sweep must not be a secret: status and the next digest carry
|
|
614
|
-
// it until a sweep succeeds. The date gate above still holds so a broken
|
|
615
|
-
// repo errors once a day, not hourly.
|
|
616
|
-
if (receipt.reap_error) state.last_reap_error = { date: today, error: receipt.reap_error };
|
|
617
|
-
else delete state.last_reap_error;
|
|
941
|
+
state.last_mission_cleanup_date = today;
|
|
618
942
|
}
|
|
619
943
|
autoland.writeState(root, state);
|
|
620
944
|
|
|
945
|
+
try {
|
|
946
|
+
persistTickReceipt(root, receipt);
|
|
947
|
+
} catch (err) {
|
|
948
|
+
receipt.receipt_error = String((err && err.message) || err).slice(0, 200);
|
|
949
|
+
}
|
|
950
|
+
|
|
621
951
|
if (json) console.log(JSON.stringify(receipt));
|
|
622
952
|
else {
|
|
623
|
-
const
|
|
953
|
+
const reapedTotal = Number(receipt.reaped?.branches || 0) + Number(receipt.reaped?.worktrees || 0);
|
|
954
|
+
const reapNote = reapedTotal > 0 ? `, reaped ${receipt.reaped.branches} landed/overdue branches` : '';
|
|
624
955
|
const janitorNote = `, janitor stopped ${receipt.missions_stopped} mission${receipt.missions_stopped === 1 ? '' : 's'} + reaped ${receipt.worktrees_reaped} worktree${receipt.worktrees_reaped === 1 ? '' : 's'}`;
|
|
625
|
-
|
|
956
|
+
const heldNote = receipt.missions_held ? `, held ${receipt.missions_held} human-blocked mission${receipt.missions_held === 1 ? '' : 's'}` : '';
|
|
957
|
+
const wishNote = `, ${wishSweepSummaryLine(receipt.wish_dispatch, receipt.wish_dispatch_error)}`;
|
|
958
|
+
const receiptNote = receipt.receipt_path ? `, receipt ${receipt.receipt_path}` : '';
|
|
959
|
+
const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${receiptNote}`;
|
|
960
|
+
console.log(gateForHuman(summary).text);
|
|
961
|
+
for (const fulfilled of receipt.wish_dispatch?.fulfilled_results || []) {
|
|
962
|
+
if (fulfilled.review_ask) console.log(fulfilled.review_ask);
|
|
963
|
+
}
|
|
626
964
|
}
|
|
627
965
|
return 0;
|
|
628
966
|
}
|
|
629
967
|
|
|
630
968
|
function showHelp() {
|
|
631
969
|
console.log('');
|
|
632
|
-
console.log('atris autoland
|
|
970
|
+
console.log('atris autoland: you approve the policy once; certified work lands itself');
|
|
633
971
|
console.log('');
|
|
634
972
|
console.log('finished work that passed its checks and two independent reviews lands');
|
|
635
973
|
console.log('automatically with a receipt. money, deploys, security, customer, and');
|
|
636
974
|
console.log('outward-facing work always waits for you.');
|
|
637
975
|
console.log('');
|
|
976
|
+
console.log('recommended agent flight checks:');
|
|
977
|
+
console.log(' fast suite: npm run test:fast');
|
|
978
|
+
console.log(' focused: node --test <focused files>');
|
|
979
|
+
console.log('record one runnable verifier; autoland reruns the recorded check.');
|
|
980
|
+
console.log('');
|
|
638
981
|
console.log(' atris autoland what would land, what waits for you');
|
|
639
982
|
console.log(' atris autoland on [--to <phone>] flip it on: hourly heartbeat, daily');
|
|
640
983
|
console.log(' message, ping when something waits');
|
|
@@ -672,8 +1015,17 @@ function autolandCommand(args = []) {
|
|
|
672
1015
|
module.exports = {
|
|
673
1016
|
autolandCommand,
|
|
674
1017
|
attachedTasksForMission,
|
|
1018
|
+
digestNextMoves,
|
|
1019
|
+
digestStoryRows,
|
|
1020
|
+
digestTickStatus,
|
|
1021
|
+
explainResult,
|
|
675
1022
|
missionReadyForClosedTaskVerify,
|
|
676
1023
|
operatorReady,
|
|
1024
|
+
readAcceptedTaskHistory,
|
|
677
1025
|
hasAgentJargon,
|
|
1026
|
+
runTickBody,
|
|
1027
|
+
persistTickReceipt,
|
|
1028
|
+
sweepLanding,
|
|
678
1029
|
verifyClosedTaskMissions,
|
|
1030
|
+
wishSweepSummaryLine,
|
|
679
1031
|
};
|