atris 3.56.3 → 3.57.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/commands/integrations.js +115 -9
- package/commands/worktree.js +155 -11
- package/lib/conductor-artifacts.js +3 -1
- package/package.json +1 -1
package/commands/integrations.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* atris gmail inbox [--account <id>] - List recent emails for a mailbox
|
|
6
6
|
* atris gmail read <id> [--account <id>] - Read specific email
|
|
7
7
|
* atris gmail archive <id> [...] [--account <id>] - Archive messages
|
|
8
|
-
* atris gmail
|
|
8
|
+
* atris gmail triage [--account <id>] [--limit N] - Record keep or archive verdicts
|
|
9
|
+
* atris gmail verdicts [--account <id>] [--limit N] - List recent Gmail verdicts
|
|
9
10
|
* atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
|
|
10
11
|
* atris gmail voice [account] [--clear] - Edit an account's writing voice
|
|
11
12
|
* atris gmail connect [name] - Connect or reconnect a Gmail account
|
|
@@ -40,7 +41,18 @@ const GMAIL_CONNECT_POLL_MS = 3000;
|
|
|
40
41
|
const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
41
42
|
const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
|
|
42
43
|
const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
|
|
44
|
+
const GMAIL_TRIAGE_USAGE = 'usage: atris gmail triage [--account <id>] [--limit N]';
|
|
43
45
|
const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--account <id>] [--limit N]';
|
|
46
|
+
const GMAIL_BULK_DOMAINS = [
|
|
47
|
+
'campaign-archive.com',
|
|
48
|
+
'constantcontact.com',
|
|
49
|
+
'hubspotemail.net',
|
|
50
|
+
'mailchimp.com',
|
|
51
|
+
'mailchimpapp.net',
|
|
52
|
+
'mailgun.org',
|
|
53
|
+
'sendgrid.net',
|
|
54
|
+
'substack.com',
|
|
55
|
+
];
|
|
44
56
|
|
|
45
57
|
function gmailVerdictsPath(root = process.cwd()) {
|
|
46
58
|
return path.join(root, '.atris', 'state', 'gmail-verdicts.jsonl');
|
|
@@ -54,7 +66,7 @@ function appendGmailVerdicts(verdicts, options = {}) {
|
|
|
54
66
|
const lines = rows.map((row) => JSON.stringify({
|
|
55
67
|
ts: row.ts || new Date().toISOString(),
|
|
56
68
|
account: String(row.account || ''),
|
|
57
|
-
verdict: 'archive',
|
|
69
|
+
verdict: row.verdict === 'keep' ? 'keep' : 'archive',
|
|
58
70
|
message_id: String(row.message_id || ''),
|
|
59
71
|
...(row.from ? { from: row.from } : {}),
|
|
60
72
|
...(row.subject ? { subject: row.subject } : {}),
|
|
@@ -90,11 +102,28 @@ function printGmailVerdicts(options = {}) {
|
|
|
90
102
|
}
|
|
91
103
|
for (const row of rows) {
|
|
92
104
|
const details = [row.from && `from ${row.from}`, row.subject && `subject ${row.subject}`].filter(Boolean);
|
|
93
|
-
console.log(`${row.ts} archive ${row.message_id} account ${row.account}${details.length ? `, ${details.join(', ')}` : ''}`);
|
|
105
|
+
console.log(`${row.ts} ${row.verdict || 'archive'} ${row.message_id} account ${row.account}${details.length ? `, ${details.join(', ')}` : ''}`);
|
|
94
106
|
}
|
|
95
107
|
return rows;
|
|
96
108
|
}
|
|
97
109
|
|
|
110
|
+
function parseGmailTriageArgs(args = []) {
|
|
111
|
+
let accountId = null;
|
|
112
|
+
let limit = 25;
|
|
113
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
114
|
+
const arg = args[i];
|
|
115
|
+
const value = String(args[i + 1] || '').trim();
|
|
116
|
+
if (arg === '--account' && value && !value.startsWith('--')) accountId = value;
|
|
117
|
+
else if (arg === '--limit' && /^\d+$/.test(value) && Number(value) > 0) limit = Number(value);
|
|
118
|
+
else {
|
|
119
|
+
console.error(GMAIL_TRIAGE_USAGE);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
return { accountId, limit };
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
function parseGmailVerdictsArgs(args = []) {
|
|
99
128
|
let account = null;
|
|
100
129
|
let limit = 20;
|
|
@@ -278,8 +307,10 @@ async function gmailInbox(options = {}) {
|
|
|
278
307
|
const account = findGmailAccount(accounts, accountId);
|
|
279
308
|
const mailboxEmail = account ? gmailAccountIdentity(account).email : '';
|
|
280
309
|
|
|
281
|
-
if (
|
|
282
|
-
|
|
310
|
+
if (!options.quiet) {
|
|
311
|
+
if (mailboxEmail) console.log(`inbox for ${mailboxEmail} (${accountId.toLowerCase()})`);
|
|
312
|
+
console.log('📬 Fetching inbox...\n');
|
|
313
|
+
}
|
|
283
314
|
|
|
284
315
|
const path = `/integrations/gmail/messages?max_results=${limit}&account_id=${encodeURIComponent(accountId)}`;
|
|
285
316
|
|
|
@@ -299,13 +330,16 @@ async function gmailInbox(options = {}) {
|
|
|
299
330
|
process.exit(1);
|
|
300
331
|
}
|
|
301
332
|
|
|
302
|
-
const
|
|
333
|
+
const payload = result.data?.messages || result.data || [];
|
|
334
|
+
const messages = Array.isArray(payload) ? payload : [];
|
|
303
335
|
|
|
304
336
|
if (messages.length === 0) {
|
|
305
|
-
console.log('No messages found.');
|
|
306
|
-
return;
|
|
337
|
+
if (!options.quiet) console.log('No messages found.');
|
|
338
|
+
return messages;
|
|
307
339
|
}
|
|
308
340
|
|
|
341
|
+
if (options.quiet) return messages;
|
|
342
|
+
|
|
309
343
|
console.log(`Found ${messages.length} messages:\n`);
|
|
310
344
|
console.log('─'.repeat(60));
|
|
311
345
|
|
|
@@ -321,6 +355,69 @@ async function gmailInbox(options = {}) {
|
|
|
321
355
|
console.log(`ID: ${id}`);
|
|
322
356
|
console.log('─'.repeat(60));
|
|
323
357
|
}
|
|
358
|
+
|
|
359
|
+
return messages;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function gmailMessageHeaderText(message = {}) {
|
|
363
|
+
const lines = [];
|
|
364
|
+
const headers = message.headers;
|
|
365
|
+
if (Array.isArray(headers)) {
|
|
366
|
+
for (const header of headers) {
|
|
367
|
+
lines.push(`${String(header?.name || '')}: ${String(header?.value || '')}`);
|
|
368
|
+
}
|
|
369
|
+
} else if (headers && typeof headers === 'object') {
|
|
370
|
+
for (const [name, value] of Object.entries(headers)) lines.push(`${name}: ${String(value || '')}`);
|
|
371
|
+
} else if (headers) {
|
|
372
|
+
lines.push(String(headers));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const name of ['list_id', 'list_unsubscribe', 'mailing_list', 'unsubscribe']) {
|
|
376
|
+
if (message[name]) lines.push(`${name}: ${String(message[name])}`);
|
|
377
|
+
}
|
|
378
|
+
return lines.join('\n').toLowerCase();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function gmailTriageVerdict(message = {}) {
|
|
382
|
+
const from = String(message.from || message.sender || '').toLowerCase();
|
|
383
|
+
if (/(?:^|[^a-z0-9])(?:no[-_.]?reply|do[-_.]?not[-_.]?reply)(?:[^a-z0-9]|$)/i.test(from)) {
|
|
384
|
+
return 'archive';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const domain = from.match(/@([a-z0-9.-]+)/i)?.[1] || '';
|
|
388
|
+
if (GMAIL_BULK_DOMAINS.some((bulkDomain) => domain === bulkDomain || domain.endsWith(`.${bulkDomain}`))) {
|
|
389
|
+
return 'archive';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const headers = gmailMessageHeaderText(message);
|
|
393
|
+
if (/(?:^|\n)(?:list-id|list-unsubscribe|mailing-list|x-campaign-id|x-mailing-list)\s*:/i.test(headers)
|
|
394
|
+
|| /(?:^|\n)precedence\s*:\s*(?:bulk|junk|list)/i.test(headers)
|
|
395
|
+
|| /\bunsubscribe\b/i.test(headers)) {
|
|
396
|
+
return 'archive';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return 'keep';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function gmailTriage(options = {}) {
|
|
403
|
+
const accountId = resolveGmailAccountId(options.accountId);
|
|
404
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 25;
|
|
405
|
+
const messages = await gmailInbox({ accountId, limit, quiet: true });
|
|
406
|
+
const ts = new Date().toISOString();
|
|
407
|
+
const rows = messages.map((message) => ({
|
|
408
|
+
ts,
|
|
409
|
+
account: accountId,
|
|
410
|
+
verdict: gmailTriageVerdict(message),
|
|
411
|
+
message_id: String(message.id || message.message_id || ''),
|
|
412
|
+
from: String(message.from || message.sender || 'Unknown'),
|
|
413
|
+
subject: String(message.subject || '(no subject)'),
|
|
414
|
+
}));
|
|
415
|
+
appendGmailVerdicts(rows, { root: options.root, filePath: options.filePath });
|
|
416
|
+
|
|
417
|
+
const keepCount = rows.filter((row) => row.verdict === 'keep').length;
|
|
418
|
+
const archiveCount = rows.length - keepCount;
|
|
419
|
+
console.log(`gmail triage recorded ${keepCount} keep and ${archiveCount} archive verdict${rows.length === 1 ? '' : 's'}.`);
|
|
420
|
+
return rows;
|
|
324
421
|
}
|
|
325
422
|
|
|
326
423
|
async function gmailRead(messageId, options = {}) {
|
|
@@ -700,6 +797,11 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
700
797
|
await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
|
|
701
798
|
break;
|
|
702
799
|
}
|
|
800
|
+
case 'triage': {
|
|
801
|
+
const parsed = parseGmailTriageArgs(args);
|
|
802
|
+
await gmailTriage(parsed);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
703
805
|
case 'verdicts': {
|
|
704
806
|
const parsed = parseGmailVerdictsArgs(args);
|
|
705
807
|
printGmailVerdicts(parsed);
|
|
@@ -729,7 +831,8 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
729
831
|
console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
|
|
730
832
|
console.log(' atris gmail read <id> [--account <id>] - read specific email');
|
|
731
833
|
console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
|
|
732
|
-
console.log(' atris gmail
|
|
834
|
+
console.log(' atris gmail triage [--account <id>] [--limit N] - record keep or archive verdicts without changing mail');
|
|
835
|
+
console.log(' atris gmail verdicts [--account <id>] [--limit N] - list recent gmail verdicts');
|
|
733
836
|
console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
|
|
734
837
|
console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
|
|
735
838
|
console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
|
|
@@ -2253,6 +2356,9 @@ module.exports = {
|
|
|
2253
2356
|
appendGmailVerdicts,
|
|
2254
2357
|
readGmailVerdicts,
|
|
2255
2358
|
printGmailVerdicts,
|
|
2359
|
+
parseGmailTriageArgs,
|
|
2360
|
+
gmailTriageVerdict,
|
|
2361
|
+
gmailTriage,
|
|
2256
2362
|
calendarCommand,
|
|
2257
2363
|
twitterCommand,
|
|
2258
2364
|
slackCommand,
|
package/commands/worktree.js
CHANGED
|
@@ -358,11 +358,14 @@ function statusCounts(root, {
|
|
|
358
358
|
ignoredUnstagedFiles = new Set(),
|
|
359
359
|
ignoredUntrackedFiles = new Set(),
|
|
360
360
|
ignoreUntracked = null,
|
|
361
|
+
ignoreAny = null,
|
|
361
362
|
} = {}) {
|
|
362
363
|
if (!fs.existsSync(root)) return null;
|
|
363
364
|
// -uall expands untracked directories to individual files so ignoredUntrackedFiles
|
|
364
365
|
// and ignoreUntracked can match exact paths (plain porcelain collapses them to "?? dir/").
|
|
365
|
-
const expandUntracked = ignoredUntrackedFiles.size
|
|
366
|
+
const expandUntracked = ignoredUntrackedFiles.size
|
|
367
|
+
|| typeof ignoreUntracked === 'function'
|
|
368
|
+
|| typeof ignoreAny === 'function';
|
|
366
369
|
const statusArgs = expandUntracked ? ['status', '--porcelain', '-uall'] : ['status', '--porcelain'];
|
|
367
370
|
const result = runGit(statusArgs, { cwd: root, check: false });
|
|
368
371
|
if (result.status !== 0) return null;
|
|
@@ -371,6 +374,7 @@ function statusCounts(root, {
|
|
|
371
374
|
let untracked = 0;
|
|
372
375
|
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
373
376
|
const file = line.slice(3);
|
|
377
|
+
if (typeof ignoreAny === 'function' && ignoreAny(file)) continue;
|
|
374
378
|
if (ignoredUnstagedFiles.has(file) && line[0] === ' ' && line[1] !== ' ') continue;
|
|
375
379
|
if (line.startsWith('??')) {
|
|
376
380
|
if (ignoredUntrackedFiles.has(file)) continue;
|
|
@@ -457,6 +461,16 @@ function printStatus() {
|
|
|
457
461
|
function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task, branch: branchOverride, path: pathOverride, base: baseOverride, now = new Date() } = {}) {
|
|
458
462
|
const owner = member || agent;
|
|
459
463
|
if (!owner || !task) throw new Error('createAgentWorktree: owner (member/agent) and task required');
|
|
464
|
+
// Every new flight pays down completed checkout debt before adding another
|
|
465
|
+
// full repo copy. Branches and commits survive worktree removal, while real
|
|
466
|
+
// uncommitted changes remain protected by cleanupWorktrees.
|
|
467
|
+
let reapedBeforeStart = [];
|
|
468
|
+
let cachePrunedBeforeStart = [];
|
|
469
|
+
try {
|
|
470
|
+
const cleaned = cleanupWorktrees({ root, base: defaultMainlineBase(root), apply: true });
|
|
471
|
+
reapedBeforeStart = cleaned.removed;
|
|
472
|
+
cachePrunedBeforeStart = cleaned.cachePruned;
|
|
473
|
+
} catch {}
|
|
460
474
|
const branch = branchOverride || branchName(owner, task, now);
|
|
461
475
|
const target = path.resolve(pathOverride || defaultWorktreePath(root, owner, task, now));
|
|
462
476
|
const explicitBase = Boolean(baseOverride);
|
|
@@ -489,7 +503,7 @@ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task,
|
|
|
489
503
|
}, null, 2) + '\n',
|
|
490
504
|
'utf8'
|
|
491
505
|
);
|
|
492
|
-
return { path: target, branch, base: shipBase, checkoutBase, owner };
|
|
506
|
+
return { path: target, branch, base: shipBase, checkoutBase, owner, reapedBeforeStart, cachePrunedBeforeStart };
|
|
493
507
|
}
|
|
494
508
|
|
|
495
509
|
function startWorktree(args) {
|
|
@@ -543,6 +557,13 @@ function startWorktree(args) {
|
|
|
543
557
|
}
|
|
544
558
|
const { path: target, branch, base } = created;
|
|
545
559
|
|
|
560
|
+
if (created.reapedBeforeStart.length) {
|
|
561
|
+
console.log(`cleanup: reaped ${created.reapedBeforeStart.length} completed ${created.reapedBeforeStart.length === 1 ? 'worktree' : 'worktrees'}`);
|
|
562
|
+
}
|
|
563
|
+
if (created.cachePrunedBeforeStart.length) {
|
|
564
|
+
console.log(`cleanup: pruned ${created.cachePrunedBeforeStart.length} generated ${created.cachePrunedBeforeStart.length === 1 ? 'cache' : 'caches'}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
546
567
|
const counts = statusCounts(root);
|
|
547
568
|
if (counts && (counts.staged || counts.unstaged || counts.untracked)) {
|
|
548
569
|
console.log(`note: primary checkout is dirty staged=${counts.staged} unstaged=${counts.unstaged} untracked=${counts.untracked}`);
|
|
@@ -889,6 +910,14 @@ const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
|
889
910
|
// (zero commits yet), so without a grace window the janitor reaps it while
|
|
890
911
|
// the engine that requested it is still booting inside.
|
|
891
912
|
const WORKTREE_REAP_GRACE_MS = 60 * 60 * 1000;
|
|
913
|
+
const COMPLETED_UNMERGED_REAP_MS = WORKTREE_REAP_GRACE_MS;
|
|
914
|
+
const GENERATED_WORKTREE_CACHE_DIRS = [
|
|
915
|
+
'.derivedData',
|
|
916
|
+
'.next',
|
|
917
|
+
'.swiftpm',
|
|
918
|
+
'.cache/swiftpm',
|
|
919
|
+
'node_modules/.cache',
|
|
920
|
+
];
|
|
892
921
|
|
|
893
922
|
function worktreeWithinReapGrace(worktreePath, now = Date.now()) {
|
|
894
923
|
try {
|
|
@@ -898,15 +927,84 @@ function worktreeWithinReapGrace(worktreePath, now = Date.now()) {
|
|
|
898
927
|
}
|
|
899
928
|
}
|
|
900
929
|
|
|
901
|
-
function
|
|
930
|
+
function completedAgentOutputAgeMs(worktreePath, now = Date.now()) {
|
|
931
|
+
try {
|
|
932
|
+
const output = path.join(worktreePath, '.codex-last-message.txt');
|
|
933
|
+
return now - fs.statSync(output).mtimeMs;
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function canonicalPath(value) {
|
|
940
|
+
try {
|
|
941
|
+
return fs.realpathSync(value);
|
|
942
|
+
} catch {
|
|
943
|
+
return path.resolve(value);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function listActiveProcessCwds() {
|
|
948
|
+
const result = spawnSync('lsof', ['-n', '-a', '-d', 'cwd', '-F', 'n'], {
|
|
949
|
+
encoding: 'utf8',
|
|
950
|
+
maxBuffer: COMMAND_MAX_BUFFER_BYTES,
|
|
951
|
+
});
|
|
952
|
+
if (result.status !== 0) return [];
|
|
953
|
+
return String(result.stdout || '')
|
|
954
|
+
.split(/\r?\n/)
|
|
955
|
+
.filter((line) => line.startsWith('n'))
|
|
956
|
+
.map((line) => canonicalPath(line.slice(1)))
|
|
957
|
+
.filter(Boolean);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function worktreeHasActiveProcess(worktreePath, activeCwds) {
|
|
961
|
+
const resolved = canonicalPath(worktreePath);
|
|
962
|
+
const prefix = `${resolved}${path.sep}`;
|
|
963
|
+
return activeCwds.some((cwd) => {
|
|
964
|
+
const active = canonicalPath(cwd);
|
|
965
|
+
return active === resolved || active.startsWith(prefix);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function pruneGeneratedWorktreeCaches(worktreePath, { apply = false } = {}) {
|
|
970
|
+
const candidates = [];
|
|
971
|
+
const pruned = [];
|
|
972
|
+
for (const relative of GENERATED_WORKTREE_CACHE_DIRS) {
|
|
973
|
+
const target = path.join(worktreePath, relative);
|
|
974
|
+
if (!fs.existsSync(target)) continue;
|
|
975
|
+
const ignored = runGit(['check-ignore', '-q', '--', relative], { cwd: worktreePath, check: false });
|
|
976
|
+
if (ignored.status !== 0) continue;
|
|
977
|
+
const item = { worktree: worktreePath, path: target, relative };
|
|
978
|
+
candidates.push(item);
|
|
979
|
+
if (!apply) continue;
|
|
980
|
+
try {
|
|
981
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
982
|
+
pruned.push(item);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
item.error = String((error && error.message) || error);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return { candidates, pruned };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply = false, activeCwds: activeCwdsOverride } = {}) {
|
|
902
991
|
const worktrees = listWorktrees(root);
|
|
903
992
|
const primary = worktrees[0]?.path ? path.resolve(worktrees[0].path) : '';
|
|
904
993
|
const current = path.resolve(root);
|
|
994
|
+
const activeCwds = Array.isArray(activeCwdsOverride) ? activeCwdsOverride.map(canonicalPath) : listActiveProcessCwds();
|
|
905
995
|
const base = normalizeTargetRef(root, baseOverride || defaultShipTarget(root));
|
|
906
996
|
refreshRemoteRef(root, base);
|
|
907
997
|
const candidates = [];
|
|
908
998
|
const kept = [];
|
|
909
999
|
const removed = [];
|
|
1000
|
+
const cacheCandidates = [];
|
|
1001
|
+
const cachePruned = [];
|
|
1002
|
+
const pruneCachesForKept = (wtPath) => {
|
|
1003
|
+
if (worktreeWithinReapGrace(wtPath)) return;
|
|
1004
|
+
const caches = pruneGeneratedWorktreeCaches(wtPath, { apply });
|
|
1005
|
+
cacheCandidates.push(...caches.candidates);
|
|
1006
|
+
cachePruned.push(...caches.pruned);
|
|
1007
|
+
};
|
|
910
1008
|
|
|
911
1009
|
for (const wt of worktrees) {
|
|
912
1010
|
const wtPath = path.resolve(wt.path);
|
|
@@ -927,32 +1025,72 @@ function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply =
|
|
|
927
1025
|
kept.push({ ...item, reason: 'protected_branch' });
|
|
928
1026
|
continue;
|
|
929
1027
|
}
|
|
930
|
-
|
|
931
|
-
|
|
1028
|
+
if (worktreeHasActiveProcess(wtPath, activeCwds)) {
|
|
1029
|
+
kept.push({ ...item, reason: 'active_process' });
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const rawCounts = statusCounts(wt.path);
|
|
1033
|
+
const counts = statusCounts(wt.path, { ignoreAny: isConductorArtifact });
|
|
1034
|
+
if (!rawCounts || !counts) {
|
|
932
1035
|
kept.push({ ...item, reason: 'missing_or_unreadable' });
|
|
933
1036
|
continue;
|
|
934
1037
|
}
|
|
935
1038
|
if (counts.staged || counts.unstaged || counts.untracked) {
|
|
1039
|
+
pruneCachesForKept(wtPath);
|
|
936
1040
|
kept.push({ ...item, reason: 'dirty', staged: counts.staged, unstaged: counts.unstaged, untracked: counts.untracked });
|
|
937
1041
|
continue;
|
|
938
1042
|
}
|
|
939
1043
|
if (!wt.head) {
|
|
1044
|
+
pruneCachesForKept(wtPath);
|
|
940
1045
|
kept.push({ ...item, reason: 'missing_head' });
|
|
941
1046
|
continue;
|
|
942
1047
|
}
|
|
1048
|
+
const artifactOnlyDirty = Boolean(
|
|
1049
|
+
(rawCounts.staged || rawCounts.unstaged || rawCounts.untracked)
|
|
1050
|
+
&& !(counts.staged || counts.unstaged || counts.untracked)
|
|
1051
|
+
);
|
|
943
1052
|
const merged = runGit(['merge-base', '--is-ancestor', wt.head, base], { cwd: root, check: false }).status === 0;
|
|
944
1053
|
if (!merged) {
|
|
945
|
-
|
|
1054
|
+
const completedAgeMs = completedAgentOutputAgeMs(wtPath);
|
|
1055
|
+
if (completedAgeMs === null || completedAgeMs < COMPLETED_UNMERGED_REAP_MS) {
|
|
1056
|
+
pruneCachesForKept(wtPath);
|
|
1057
|
+
kept.push({
|
|
1058
|
+
...item,
|
|
1059
|
+
reason: completedAgeMs === null ? 'unmerged' : 'completed_unmerged_retention',
|
|
1060
|
+
...(completedAgeMs === null ? {} : { retention_hours: 1 }),
|
|
1061
|
+
});
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
const candidate = {
|
|
1065
|
+
...item,
|
|
1066
|
+
reason: 'completed_unmerged_checkout_expired',
|
|
1067
|
+
branch_preserved: true,
|
|
1068
|
+
artifact_only_dirty: artifactOnlyDirty,
|
|
1069
|
+
};
|
|
1070
|
+
candidates.push(candidate);
|
|
1071
|
+
if (!apply) continue;
|
|
1072
|
+
const removeArgs = ['worktree', 'remove'];
|
|
1073
|
+
if (artifactOnlyDirty) removeArgs.push('--force');
|
|
1074
|
+
removeArgs.push(wt.path);
|
|
1075
|
+
const removedResult = runGit(removeArgs, { cwd: root, check: false });
|
|
1076
|
+
if (removedResult.status === 0) {
|
|
1077
|
+
removed.push(candidate);
|
|
1078
|
+
} else {
|
|
1079
|
+
kept.push({ ...item, reason: 'remove_failed', error: (removedResult.stderr || removedResult.stdout || '').trim() });
|
|
1080
|
+
}
|
|
946
1081
|
continue;
|
|
947
1082
|
}
|
|
948
1083
|
if (worktreeWithinReapGrace(wtPath)) {
|
|
949
1084
|
kept.push({ ...item, reason: 'fresh_worktree_grace' });
|
|
950
1085
|
continue;
|
|
951
1086
|
}
|
|
952
|
-
const candidate = { ...item, reason: 'merged_into_base' };
|
|
1087
|
+
const candidate = { ...item, reason: 'merged_into_base', artifact_only_dirty: artifactOnlyDirty };
|
|
953
1088
|
candidates.push(candidate);
|
|
954
1089
|
if (!apply) continue;
|
|
955
|
-
const
|
|
1090
|
+
const removeArgs = ['worktree', 'remove'];
|
|
1091
|
+
if (artifactOnlyDirty) removeArgs.push('--force');
|
|
1092
|
+
removeArgs.push(wt.path);
|
|
1093
|
+
const removedResult = runGit(removeArgs, { cwd: root, check: false });
|
|
956
1094
|
if (removedResult.status === 0) {
|
|
957
1095
|
removed.push(candidate);
|
|
958
1096
|
} else {
|
|
@@ -960,7 +1098,7 @@ function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply =
|
|
|
960
1098
|
}
|
|
961
1099
|
}
|
|
962
1100
|
|
|
963
|
-
return { apply, base, candidates, removed, kept };
|
|
1101
|
+
return { apply, base, candidates, removed, kept, cacheCandidates, cachePruned };
|
|
964
1102
|
}
|
|
965
1103
|
|
|
966
1104
|
function cleanup(args) {
|
|
@@ -980,6 +1118,11 @@ function cleanup(args) {
|
|
|
980
1118
|
for (const item of result.apply ? result.removed : result.candidates) {
|
|
981
1119
|
console.log(`${action}: ${item.path} branch=${item.branch} reason=${item.reason}`);
|
|
982
1120
|
}
|
|
1121
|
+
const cacheAction = result.apply ? 'cache_pruned' : 'cache_candidate';
|
|
1122
|
+
console.log(`${cacheAction}s: ${result.apply ? result.cachePruned.length : result.cacheCandidates.length}`);
|
|
1123
|
+
for (const item of result.apply ? result.cachePruned : result.cacheCandidates) {
|
|
1124
|
+
console.log(`${cacheAction}: ${item.path}`);
|
|
1125
|
+
}
|
|
983
1126
|
console.log(`kept: ${result.kept.length}`);
|
|
984
1127
|
if (!result.apply && result.candidates.length) console.log('next: atris worktree cleanup --apply');
|
|
985
1128
|
return 0;
|
|
@@ -1010,11 +1153,12 @@ function guide() {
|
|
|
1010
1153
|
console.log(' --target <ref> overrides the default landing target (default: branch atris-base, else origin default branch)');
|
|
1011
1154
|
console.log(' recommended verify: npm run test:fast && node --test <focused files>');
|
|
1012
1155
|
console.log('');
|
|
1013
|
-
console.log('5.
|
|
1156
|
+
console.log('5. Cleanup is part of creation and the janitor; run it directly any time:');
|
|
1014
1157
|
console.log(' atris worktree cleanup');
|
|
1015
1158
|
console.log(' atris worktree cleanup --apply');
|
|
1016
1159
|
console.log('');
|
|
1017
1160
|
console.log('Notes: start uses the current upstream/default remote base, not dirty local HEAD.');
|
|
1161
|
+
console.log('Start reaps completed checkouts first. Cleanup preserves branches and source changes, skips live work, and prunes only ignored build caches.');
|
|
1018
1162
|
console.log('Use `atris worktree status` to see all worktrees and dirty counts.');
|
|
1019
1163
|
return 0;
|
|
1020
1164
|
}
|
|
@@ -1032,7 +1176,7 @@ function help() {
|
|
|
1032
1176
|
console.log(' atris worktree status');
|
|
1033
1177
|
console.log(' atris worktree guard [--allow-primary] [--allow-dirty]');
|
|
1034
1178
|
console.log(' atris worktree prune [--apply]');
|
|
1035
|
-
console.log(' atris worktree cleanup [--apply] [--json] [--base origin/master]');
|
|
1179
|
+
console.log(' atris worktree cleanup [--apply] [--json] [--base origin/master] reap completed checkouts and ignored build caches');
|
|
1036
1180
|
}
|
|
1037
1181
|
|
|
1038
1182
|
function worktreeCommand(args = []) {
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
// by its own prompt file.
|
|
8
8
|
const CONDUCTOR_UNTRACKED_PATTERN =
|
|
9
9
|
/^\.atris\/(?:agent-worktree\.json|fleet-prompt-[^/]+\.md|codex-watchdog\.js|codex-watchdog-[^/]+\.json|runtime-tmp(?:\/.*)?|state\/briefs\.jsonl)$/;
|
|
10
|
+
const CONDUCTOR_ROOT_OUTPUT_PATTERN = /^\.codex-last-message\.txt$/;
|
|
10
11
|
|
|
11
12
|
function isConductorArtifact(file) {
|
|
12
|
-
|
|
13
|
+
const value = String(file || '');
|
|
14
|
+
return CONDUCTOR_UNTRACKED_PATTERN.test(value) || CONDUCTOR_ROOT_OUTPUT_PATTERN.test(value);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
// Accepts a `git status --porcelain` line ("?? .atris/fleet-prompt-BCK-1.md").
|