atris 3.74.28 → 3.75.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/atris/skills/calendar/SKILL.md +9 -0
- package/atris/skills/drive/SKILL.md +7 -0
- package/atris/skills/email-agent/SKILL.md +7 -0
- package/atris/skills/magic-inbox/SKILL.md +7 -0
- package/atris/skills/slides/SKILL.md +7 -0
- package/bin/atris.js +14 -2
- package/commands/engine.js +2 -2
- package/commands/integrations.js +539 -54
- package/commands/mission.js +2 -2
- package/commands/rating.js +14 -4
- package/commands/worktree.js +39 -12
- package/lib/fleet.js +584 -81
- package/lib/gm-season.js +31 -9
- package/lib/known-commands.js +1 -1
- package/lib/release-kind.js +31 -7
- package/lib/task-db.js +185 -26
- package/lib/worktree-space.js +4 -45
- package/package.json +1 -1
package/commands/mission.js
CHANGED
|
@@ -10256,7 +10256,7 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
10256
10256
|
// safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
|
|
10257
10257
|
// full loop, see lib/fleet.js. --dry-run previews the staffing only.
|
|
10258
10258
|
if (hasFlag(args, '--fleet')) {
|
|
10259
|
-
const { runFleetFlight } = require('../lib/fleet');
|
|
10259
|
+
const { runFleetFlight, flightExitCode } = require('../lib/fleet');
|
|
10260
10260
|
const slots = Math.max(1, Number(readFlag(args, '--slots', '')) || 3);
|
|
10261
10261
|
// Default: cut build worktrees from origin/master (checkoutBase). --base
|
|
10262
10262
|
// keeps launcher-HEAD only when the operator asks for it explicitly.
|
|
@@ -10268,7 +10268,7 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
10268
10268
|
...(baseOverride ? { checkoutBase: baseOverride } : {}),
|
|
10269
10269
|
});
|
|
10270
10270
|
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
10271
|
-
process.exitCode = flight
|
|
10271
|
+
process.exitCode = flightExitCode(flight);
|
|
10272
10272
|
context.handled = true;
|
|
10273
10273
|
return context;
|
|
10274
10274
|
}
|
package/commands/rating.js
CHANGED
|
@@ -24,6 +24,12 @@ function rated(n) {
|
|
|
24
24
|
return `${/^(8|11$|18$)/.test(String(n)) ? 'an' : 'a'} ${n}`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Jobs the build machine could not start (no room on the disk, a build copy that would not open) were let
|
|
28
|
+
// go before any engine ran. They are no member's miss and not finished, and they say so in a line of their own.
|
|
29
|
+
function couldNotStartLine(n) {
|
|
30
|
+
return `${n} ${n === 1 ? 'job' : 'jobs'} could not start on this machine. ${n === 1 ? 'It doesn\'t' : 'They don\'t'} count against anyone.`;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
// Members rated on the last 30 days of jobs here, best first, each with the
|
|
28
34
|
// job and model the roster gives it, plus the build line: what the build
|
|
29
35
|
// engine rates in the combine and what members doing build landed here.
|
|
@@ -40,9 +46,9 @@ function readHere(deps = {}) {
|
|
|
40
46
|
if (!played || played.error || played.no_history) return null;
|
|
41
47
|
const { rosterRows } = require('../lib/gm-season');
|
|
42
48
|
// A member whose only misses were later confirmed not needed, or whose only jobs
|
|
43
|
-
// wait on a pull request, has no finished job, but still shows,
|
|
44
|
-
// have somewhere to be counted.
|
|
45
|
-
const rows = rosterRows({ outcomes: played.outcomes || [], limit: Number.MAX_SAFE_INTEGER }).filter((row) => row.finished > 0 || row.not_needed > 0 || row.held > 0);
|
|
49
|
+
// wait on a pull request or could not start, has no finished job, but still shows,
|
|
50
|
+
// so those jobs have somewhere to be counted.
|
|
51
|
+
const rows = rosterRows({ outcomes: played.outcomes || [], limit: Number.MAX_SAFE_INTEGER }).filter((row) => row.finished > 0 || row.not_needed > 0 || row.held > 0 || row.could_not_start > 0);
|
|
46
52
|
if (!rows.length) return null;
|
|
47
53
|
let lineup = deps.lineup;
|
|
48
54
|
if (lineup === undefined) lineup = require('../lib/team-lineup').readLineupSafe(root);
|
|
@@ -57,7 +63,7 @@ function readHere(deps = {}) {
|
|
|
57
63
|
}
|
|
58
64
|
const miss_reasons = [...reasons].map(([reason, count]) => ({ reason, count }))
|
|
59
65
|
.sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)).slice(0, 3);
|
|
60
|
-
return { member: row.name, ovr: row.ovr, landed: row.landed, finished: row.finished, ...(row.not_needed ? { not_needed: row.not_needed } : {}), ...(row.held ? { held: row.held } : {}), job: seat.job || null, model: seat.model || null, ...(miss_reasons.length ? { miss_reasons } : {}) };
|
|
66
|
+
return { member: row.name, ovr: row.ovr, landed: row.landed, finished: row.finished, ...(row.not_needed ? { not_needed: row.not_needed } : {}), ...(row.held ? { held: row.held } : {}), ...(row.could_not_start ? { could_not_start: row.could_not_start } : {}), job: seat.job || null, model: seat.model || null, ...(miss_reasons.length ? { miss_reasons } : {}) };
|
|
61
67
|
});
|
|
62
68
|
const buildJob = ((lineup && lineup.ok && lineup.jobs) || []).find((row) => row.job === 'build' && !row.key);
|
|
63
69
|
const buildModel = buildJob && buildJob.now_runs ? buildJob.now_runs.model : null;
|
|
@@ -65,9 +71,11 @@ function readHere(deps = {}) {
|
|
|
65
71
|
const builders = members.filter((m) => m.job === 'build');
|
|
66
72
|
const landed = builders.reduce((sum, m) => sum + m.landed, 0);
|
|
67
73
|
const finished = builders.reduce((sum, m) => sum + m.finished, 0);
|
|
74
|
+
const couldNotStart = members.reduce((sum, m) => sum + (m.could_not_start || 0), 0);
|
|
68
75
|
return {
|
|
69
76
|
workspace: played.workspace_name || null,
|
|
70
77
|
members,
|
|
78
|
+
...(couldNotStart ? { could_not_start: couldNotStart } : {}),
|
|
71
79
|
build: card && finished ? { model: buildModel, combine_ovr: card.ovr, landed, finished, ovr: rating.atrisRating(landed, finished) } : null,
|
|
72
80
|
};
|
|
73
81
|
}
|
|
@@ -98,6 +106,7 @@ function renderHere(here) {
|
|
|
98
106
|
lines.push('', `${head}${shown.join(', ')}${rest ? `, and ${rest} more` : ''}`);
|
|
99
107
|
}
|
|
100
108
|
lines.push('landed: accepted or merged. drops and abandons count against a member.');
|
|
109
|
+
if (here.could_not_start) lines.push(couldNotStartLine(here.could_not_start));
|
|
101
110
|
if (here.build) {
|
|
102
111
|
const b = here.build;
|
|
103
112
|
lines.push('', `build runs ${modelLabel(b.model)}, ${rated(b.combine_ovr)} in the combine.`,
|
|
@@ -116,6 +125,7 @@ function renderMember(m, card) {
|
|
|
116
125
|
];
|
|
117
126
|
if (m.not_needed) lines.push(` not needed: ${m.not_needed} (confirmed by someone else, so ${m.not_needed === 1 ? 'it does' : 'they do'} not count either way)`);
|
|
118
127
|
if (m.held) lines.push(` waiting on a pull request: ${m.held} (the work is done and a person has to land it, so ${m.held === 1 ? 'it does' : 'they do'} not count either way)`);
|
|
128
|
+
if (m.could_not_start) lines.push(` ${couldNotStartLine(m.could_not_start)}`);
|
|
119
129
|
if (m.ovr == null) lines.push(' rated once it has 3 finished jobs');
|
|
120
130
|
if (m.miss_reasons && m.miss_reasons.length) {
|
|
121
131
|
lines.push(' top miss reasons:');
|
package/commands/worktree.js
CHANGED
|
@@ -589,10 +589,11 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
589
589
|
// Every new flight pays down completed checkout debt before adding another
|
|
590
590
|
// full repo copy. Branches and commits survive worktree removal, while real
|
|
591
591
|
// uncommitted changes remain protected by cleanupWorktrees.
|
|
592
|
+
const cleanupBudget = { count: 0, kib: 0 };
|
|
592
593
|
let reapedBeforeStart = [];
|
|
593
594
|
let cachePrunedBeforeStart = [];
|
|
594
595
|
try {
|
|
595
|
-
const cleaned = cleanupWorktrees({ root, base: mainline, apply: true, by: 'atris worktree start' });
|
|
596
|
+
const cleaned = cleanupWorktrees({ root, base: mainline, apply: true, by: 'atris worktree start', cleanupBudget });
|
|
596
597
|
reapedBeforeStart = cleaned.removed;
|
|
597
598
|
cachePrunedBeforeStart = cleaned.cachePruned;
|
|
598
599
|
} catch {}
|
|
@@ -616,9 +617,7 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
616
617
|
const shipBase = checkoutBase;
|
|
617
618
|
if (fs.existsSync(target)) throw new Error(`worktree path already exists: ${target}`);
|
|
618
619
|
// A full disk stops every flight at once. Reap first; refuse before cutting.
|
|
619
|
-
const room = (spaceGuard
|
|
620
|
-
? ensureRoomForCopy({ root, target, freeSpace, env })
|
|
621
|
-
: null;
|
|
620
|
+
const room = checkRoomForCopy({ root, target, freeSpace, env, spaceGuard, cleanupBudget });
|
|
622
621
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
623
622
|
const staleBase = refreshRemoteRef(root, checkoutBase);
|
|
624
623
|
// A retry of the same job starts from the last run's unlanded commits; it
|
|
@@ -734,13 +733,13 @@ function seedCopyDeps({ root, target, options = {} }) {
|
|
|
734
733
|
// the engine that just finished may still have its last message unread.
|
|
735
734
|
const LOW_DISK_IDLE_MS = 10 * 60 * 1000;
|
|
736
735
|
|
|
737
|
-
function ensureRoomForCopy({ root, target, freeSpace = space.freeBytes, env = process.env, by = 'atris worktree start' }) {
|
|
736
|
+
function ensureRoomForCopy({ root, target, freeSpace = space.freeBytes, env = process.env, by = 'atris worktree start', cleanupBudget = null }) {
|
|
738
737
|
const limits = space.spaceLimits(env);
|
|
739
738
|
const before = freeSpace(target);
|
|
740
739
|
const room = { before, free: before, removed: [], cleared: [], ...limits };
|
|
741
740
|
if (typeof before !== 'number' || !Number.isFinite(before)) return room;
|
|
742
741
|
if (before >= limits.reapBelow && before >= limits.refuseBelow) return room;
|
|
743
|
-
const swept = sweepWorkspace({ workspace: path.dirname(findPrimaryRoot(root)), apply: true, idleMs: LOW_DISK_IDLE_MS, freeSpace, by });
|
|
742
|
+
const swept = sweepWorkspace({ workspace: path.dirname(findPrimaryRoot(root)), apply: true, idleMs: LOW_DISK_IDLE_MS, freeSpace, by, cleanupBudget });
|
|
744
743
|
for (const repo of swept.repos) {
|
|
745
744
|
room.removed.push(...(repo.removed || []));
|
|
746
745
|
room.cleared.push(...(repo.cachePruned || []));
|
|
@@ -763,6 +762,29 @@ function ensureRoomForCopy({ root, target, freeSpace = space.freeBytes, env = pr
|
|
|
763
762
|
return room;
|
|
764
763
|
}
|
|
765
764
|
|
|
765
|
+
// When the disk is checked before a copy is cut: spaceGuard says so, else anywhere but a temp folder.
|
|
766
|
+
function checkRoomForCopy({ root, target, freeSpace, env, spaceGuard, cleanupBudget = null }) {
|
|
767
|
+
return (spaceGuard ?? !space.isScratchPath(target))
|
|
768
|
+
? ensureRoomForCopy({ root, target, freeSpace, env, cleanupBudget })
|
|
769
|
+
: null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// The question worktree start asks about the disk, asked on its own so a flight can ask it once before it
|
|
773
|
+
// claims anything: the same floor, the same reaping of finished copies, the same words. null when there is
|
|
774
|
+
// room, when the disk cannot be read, or when a copy would land in a temp folder. Otherwise the sentence
|
|
775
|
+
// worktree start would refuse with, and the numbers behind it. Any other trouble is left for worktree start
|
|
776
|
+
// to report when it runs. target names the folder the build will use when it is not worktree start's usual
|
|
777
|
+
// one (a sealed review sandbox is cut in the temp folder).
|
|
778
|
+
function lowDiskRefusal({ root = repoRoot(), member = '', agent = '', task = 'flight', now = new Date(), env = process.env, freeSpace = space.freeBytes, spaceGuard, target = '' } = {}) {
|
|
779
|
+
try {
|
|
780
|
+
checkRoomForCopy({ root, target: target ? path.resolve(target) : path.resolve(defaultWorktreePath(root, member || agent, task, now)), freeSpace, env, spaceGuard });
|
|
781
|
+
return null;
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (!error || error.code !== 'ATRIS_LOW_DISK') return null;
|
|
784
|
+
return { message: error.message, free: error.space.free, needs: error.space.refuseBelow };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
766
788
|
function startWorktree(args) {
|
|
767
789
|
const root = repoRoot();
|
|
768
790
|
const member = readFlag(args, '--member');
|
|
@@ -1694,6 +1716,7 @@ function cleanupWorktrees({
|
|
|
1694
1716
|
idleMs = 0,
|
|
1695
1717
|
freshGraceMs = WORKTREE_REAP_GRACE_MS,
|
|
1696
1718
|
now = Date.now(),
|
|
1719
|
+
cleanupBudget = null,
|
|
1697
1720
|
} = {}) {
|
|
1698
1721
|
const worktrees = listWorktrees(root);
|
|
1699
1722
|
const primary = worktrees[0]?.path ? path.resolve(worktrees[0].path) : '';
|
|
@@ -1706,7 +1729,7 @@ function cleanupWorktrees({
|
|
|
1706
1729
|
const removed = [];
|
|
1707
1730
|
const cacheCandidates = [];
|
|
1708
1731
|
const cachePruned = [];
|
|
1709
|
-
const cacheBudget = { count: 0, kib: 0 };
|
|
1732
|
+
const cacheBudget = cleanupBudget || { count: 0, kib: 0 };
|
|
1710
1733
|
const scopePaths = Array.isArray(onlyPaths) ? onlyPaths.map(canonicalPath) : null;
|
|
1711
1734
|
const inScope = (wtPath) => (!scopePaths || scopePaths.includes(canonicalPath(wtPath)))
|
|
1712
1735
|
&& (!onlyUnder || space.isInside(wtPath, onlyUnder));
|
|
@@ -1847,13 +1870,12 @@ function cleanupWorktrees({
|
|
|
1847
1870
|
// Removes copies that pass every cleanup check (merged into origin's main
|
|
1848
1871
|
// branch, nothing changed, untracked, or ignored, nothing running inside)
|
|
1849
1872
|
// and are unused for idleMs; clears verified webpack caches in the idle
|
|
1850
|
-
// copies it keeps
|
|
1851
|
-
// stays.
|
|
1873
|
+
// copies it keeps. Shared installs and unmerged work stay for review.
|
|
1852
1874
|
const SWEEP_IDLE_MS = 2 * 60 * 60 * 1000;
|
|
1853
1875
|
const SWEEP_EVERY_HOURS = 3;
|
|
1854
1876
|
const SWEEP_HIDDEN_KEPT = new Set(['primary_checkout', 'current_checkout', 'out_of_scope']);
|
|
1855
1877
|
|
|
1856
|
-
function sweepWorkspace({ workspace, apply = false, idleMs = SWEEP_IDLE_MS, activeCwds, freeSpace = space.freeBytes, by = 'atris worktree sweep' } = {}) {
|
|
1878
|
+
function sweepWorkspace({ workspace, apply = false, idleMs = SWEEP_IDLE_MS, activeCwds, freeSpace = space.freeBytes, by = 'atris worktree sweep', cleanupBudget = null } = {}) {
|
|
1857
1879
|
const root = path.resolve(workspace);
|
|
1858
1880
|
const before = freeSpace(root);
|
|
1859
1881
|
const report = { workspace: root, apply, idle_ms: idleMs, before, after: before, repos: [] };
|
|
@@ -1861,6 +1883,8 @@ function sweepWorkspace({ workspace, apply = false, idleMs = SWEEP_IDLE_MS, acti
|
|
|
1861
1883
|
// Without a process list nobody can tell which copies are in use.
|
|
1862
1884
|
if (!Array.isArray(scan)) return { ...report, error: 'could not list running programs, so every copy was kept' };
|
|
1863
1885
|
const scope = path.join(root, space.AGENT_COPIES_DIR);
|
|
1886
|
+
// One limit covers the whole sweep, not each repository separately.
|
|
1887
|
+
cleanupBudget = cleanupBudget || { count: 0, kib: 0 };
|
|
1864
1888
|
for (const repo of space.agentCopyRepos(root)) {
|
|
1865
1889
|
const entry = { name: repo.name, root: repo.root };
|
|
1866
1890
|
try {
|
|
@@ -1872,7 +1896,7 @@ function sweepWorkspace({ workspace, apply = false, idleMs = SWEEP_IDLE_MS, acti
|
|
|
1872
1896
|
// A real removal scans again per repo and right before each copy goes;
|
|
1873
1897
|
// the shared scan only serves previews and tests.
|
|
1874
1898
|
const shared = Array.isArray(activeCwds) || !apply ? scan : undefined;
|
|
1875
|
-
const result = cleanupWorktrees({ root: repo.root, base, apply, activeCwds: shared, by, onlyUnder: scope, idleMs });
|
|
1899
|
+
const result = cleanupWorktrees({ root: repo.root, base, apply, activeCwds: shared, by, onlyUnder: scope, idleMs, cleanupBudget });
|
|
1876
1900
|
report.repos.push({
|
|
1877
1901
|
...entry,
|
|
1878
1902
|
base: result.base,
|
|
@@ -1881,7 +1905,9 @@ function sweepWorkspace({ workspace, apply = false, idleMs = SWEEP_IDLE_MS, acti
|
|
|
1881
1905
|
cachePruned: result.cachePruned,
|
|
1882
1906
|
cacheCandidates: result.cacheCandidates,
|
|
1883
1907
|
kept: result.kept.filter((item) => !SWEEP_HIDDEN_KEPT.has(item.reason)),
|
|
1884
|
-
|
|
1908
|
+
// Shared installs may be in use or contain local changes. Leave them
|
|
1909
|
+
// for explicit review rather than pruning by age.
|
|
1910
|
+
installsPruned: [],
|
|
1885
1911
|
});
|
|
1886
1912
|
} catch (error) {
|
|
1887
1913
|
report.repos.push({ ...entry, error: String((error && error.message) || error) });
|
|
@@ -2145,6 +2171,7 @@ module.exports = {
|
|
|
2145
2171
|
collidingFlights,
|
|
2146
2172
|
mergePrWithRetry,
|
|
2147
2173
|
createAgentWorktree,
|
|
2174
|
+
lowDiskRefusal,
|
|
2148
2175
|
deniedLaneForShip,
|
|
2149
2176
|
protectedPushRefusal,
|
|
2150
2177
|
createOrFindPr,
|