atris 3.33.3 → 3.35.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 +0 -2
- package/FOR_AGENTS.md +5 -3
- package/atris/skills/engines/SKILL.md +16 -7
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris.md +19 -0
- package/ax +475 -17
- package/bin/atris.js +206 -44
- package/commands/aeo.js +52 -0
- package/commands/autoland.js +313 -19
- package/commands/business.js +91 -8
- package/commands/codex-goal.js +26 -2
- package/commands/drive.js +187 -0
- package/commands/engine.js +73 -2
- package/commands/feed.js +202 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +262 -3
- package/commands/init.js +13 -1
- package/commands/integrations.js +39 -11
- package/commands/interview.js +143 -0
- package/commands/land.js +114 -13
- package/commands/lesson.js +112 -1
- package/commands/linear.js +38 -0
- package/commands/loops.js +212 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +598 -64
- package/commands/now.js +25 -1
- package/commands/radar.js +259 -14
- package/commands/serve.js +54 -0
- package/commands/status.js +50 -5
- package/commands/stripe.js +38 -0
- package/commands/supabase.js +39 -0
- package/commands/task.js +935 -71
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/worktree.js +68 -10
- package/commands/write.js +399 -0
- package/lib/auto-accept-certified.js +70 -19
- package/lib/autoland.js +39 -3
- package/lib/fleet.js +256 -13
- package/lib/memory-view.js +14 -5
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/official-cli-integration.js +174 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/review-integrity.js +147 -0
- package/lib/runner-command.js +23 -0
- package/lib/task-db.js +220 -7
- package/lib/task-proof.js +20 -0
- package/lib/task-receipt.js +93 -0
- package/package.json +1 -1
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/autoland.js
CHANGED
|
@@ -7,6 +7,9 @@ const { spawnSync } = require('child_process');
|
|
|
7
7
|
|
|
8
8
|
const autoland = require('../lib/autoland');
|
|
9
9
|
const { operatorReady, hasAgentJargon } = autoland;
|
|
10
|
+
const MISSION_AUTO_VERIFY_STATUSES = new Set(['planning', 'paused', 'ready']);
|
|
11
|
+
const CLOSED_TASK_STATUSES = new Set(['done', 'archived']);
|
|
12
|
+
const MAX_MISSION_AUTO_VERIFY_PER_TICK = 3;
|
|
10
13
|
|
|
11
14
|
function repoRoot(cwd = process.cwd()) {
|
|
12
15
|
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
|
|
@@ -62,6 +65,130 @@ function readProjection(root) {
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function compactId(value) {
|
|
69
|
+
return String(value || '').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function taskIdentityRefs(task) {
|
|
73
|
+
return [
|
|
74
|
+
task?.id,
|
|
75
|
+
task?.display_id,
|
|
76
|
+
task?.legacy_ref,
|
|
77
|
+
task?.ref,
|
|
78
|
+
task?.task_id,
|
|
79
|
+
task?.metadata?.task_id,
|
|
80
|
+
task?.metadata?.display_id,
|
|
81
|
+
task?.metadata?.ref,
|
|
82
|
+
].map(compactId).filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function taskGoalRefs(task) {
|
|
86
|
+
return [
|
|
87
|
+
task?.goal_id,
|
|
88
|
+
task?.goalId,
|
|
89
|
+
task?.mission_id,
|
|
90
|
+
task?.missionId,
|
|
91
|
+
task?.metadata?.goal_id,
|
|
92
|
+
task?.metadata?.goalId,
|
|
93
|
+
task?.metadata?.mission_id,
|
|
94
|
+
task?.metadata?.missionId,
|
|
95
|
+
task?.metadata?.goal?.id,
|
|
96
|
+
].map(compactId).filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function missionTaskRefs(mission) {
|
|
100
|
+
return [
|
|
101
|
+
...(Array.isArray(mission?.task_ids) ? mission.task_ids : []),
|
|
102
|
+
mission?.task_id,
|
|
103
|
+
mission?.current_task_id,
|
|
104
|
+
mission?.task_ref,
|
|
105
|
+
mission?.xp_task?.task_id,
|
|
106
|
+
mission?.xp_task?.ref,
|
|
107
|
+
].map(compactId).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function attachedTasksForMission(mission, tasks) {
|
|
111
|
+
const missionId = compactId(mission?.id);
|
|
112
|
+
const taskRefs = new Set(missionTaskRefs(mission));
|
|
113
|
+
return (tasks || []).filter((task) => {
|
|
114
|
+
if (missionId && taskGoalRefs(task).includes(missionId)) return true;
|
|
115
|
+
if (taskRefs.size === 0) return false;
|
|
116
|
+
return taskIdentityRefs(task).some((ref) => taskRefs.has(ref));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function taskIsClosed(task) {
|
|
121
|
+
return CLOSED_TASK_STATUSES.has(String(task?.status || '').trim().toLowerCase());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function missionHasVerifier(mission) {
|
|
125
|
+
return Boolean(String(mission?.verifier || mission?.effective_verifier || '').trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function missionReadyForClosedTaskVerify(mission, tasks) {
|
|
129
|
+
const status = String(mission?.status || '').trim().toLowerCase();
|
|
130
|
+
if (!MISSION_AUTO_VERIFY_STATUSES.has(status)) return false;
|
|
131
|
+
if (!missionHasVerifier(mission)) return false;
|
|
132
|
+
if (mission?.verifier_result?.passed === true) return false;
|
|
133
|
+
const attached = attachedTasksForMission(mission, tasks);
|
|
134
|
+
return attached.length > 0 && attached.every(taskIsClosed);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseMissionVerifyResult(id, result) {
|
|
138
|
+
let payload = null;
|
|
139
|
+
try {
|
|
140
|
+
payload = JSON.parse(result.stdout || '{}');
|
|
141
|
+
} catch {}
|
|
142
|
+
const verifier = payload?.verifier_result || null;
|
|
143
|
+
const passed = verifier ? verifier.passed === true : null;
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
result: passed === true ? 'passed' : passed === false ? 'failed' : (payload?.action || (result.status === 0 ? 'tick_recorded' : 'tick_failed')),
|
|
147
|
+
status: payload?.mission?.status || null,
|
|
148
|
+
receipt_path: payload?.receipt_path || null,
|
|
149
|
+
exit_status: result.status,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function verifyClosedTaskMissions(root, tasks, { limit = MAX_MISSION_AUTO_VERIFY_PER_TICK } = {}) {
|
|
154
|
+
const verified = [];
|
|
155
|
+
const errors = [];
|
|
156
|
+
let candidates = [];
|
|
157
|
+
try {
|
|
158
|
+
const { listMissions } = require('./mission');
|
|
159
|
+
candidates = listMissions(root).filter((mission) => missionReadyForClosedTaskVerify(mission, tasks)).slice(0, limit);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
verified,
|
|
163
|
+
errors: [{ error: String((err && err.message) || err).slice(0, 200) }],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const mission of candidates) {
|
|
168
|
+
const result = runOwnCli(root, [
|
|
169
|
+
'mission',
|
|
170
|
+
'tick',
|
|
171
|
+
mission.id,
|
|
172
|
+
'--verify',
|
|
173
|
+
'--summary',
|
|
174
|
+
'All linked repair tasks are closed; autoland re-ran the mission verifier.',
|
|
175
|
+
'--json',
|
|
176
|
+
]);
|
|
177
|
+
const outcome = parseMissionVerifyResult(mission.id, result);
|
|
178
|
+
verified.push(outcome);
|
|
179
|
+
if (result.status !== 0 || outcome.result === 'failed' || outcome.result === 'tick_failed') {
|
|
180
|
+
errors.push({
|
|
181
|
+
id: mission.id,
|
|
182
|
+
result: outcome.result,
|
|
183
|
+
status: result.status,
|
|
184
|
+
stderr: String(result.stderr || '').slice(0, 200),
|
|
185
|
+
stdout: String(result.stdout || '').slice(-500),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { verified, errors };
|
|
190
|
+
}
|
|
191
|
+
|
|
65
192
|
function landSummarySafe(root) {
|
|
66
193
|
try {
|
|
67
194
|
return require('./land').landSummary(root);
|
|
@@ -70,9 +197,10 @@ function landSummarySafe(root) {
|
|
|
70
197
|
}
|
|
71
198
|
}
|
|
72
199
|
|
|
73
|
-
function evaluateQueue(root, { strictVerify }) {
|
|
200
|
+
function evaluateQueue(root, { strictVerify, acceptAll }) {
|
|
74
201
|
const cliArgs = ['task', 'auto-accept-certified', '--dry-run', '--json', '--limit', '50'];
|
|
75
|
-
if (
|
|
202
|
+
if (acceptAll) cliArgs.push('--all');
|
|
203
|
+
else if (strictVerify === false) cliArgs.push('--no-strict-verify');
|
|
76
204
|
const result = runOwnCli(root, cliArgs);
|
|
77
205
|
try {
|
|
78
206
|
const parsed = JSON.parse(result.stdout);
|
|
@@ -92,6 +220,9 @@ function plainReason(reason) {
|
|
|
92
220
|
denied_tag_feedback: 'customer feedback — yours to approve',
|
|
93
221
|
denied_tag_voice: 'voice/comms — yours to approve',
|
|
94
222
|
needs_second_reviewer_or_third_pass: 'needs one more independent check first',
|
|
223
|
+
needs_independent_reviewer: 'built and judged by the same actor, needs an independent check',
|
|
224
|
+
verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
|
|
225
|
+
judge_equals_worker: 'built and judged by the same actor, hand the review to someone else',
|
|
95
226
|
not_agent_certified: 'not certified yet',
|
|
96
227
|
insufficient_review_passes: 'not enough review passes yet',
|
|
97
228
|
strict_verify_missing: 'no recorded check command to re-run',
|
|
@@ -106,7 +237,8 @@ function showStatus(root, args) {
|
|
|
106
237
|
const policy = autoland.readPolicy(root);
|
|
107
238
|
const enabled = Boolean(policy && policy.enabled);
|
|
108
239
|
const strictVerify = policy ? policy.strict_verify !== false : true;
|
|
109
|
-
const
|
|
240
|
+
const acceptAll = Boolean(policy && policy.accept_all);
|
|
241
|
+
const results = evaluateQueue(root, { strictVerify, acceptAll })
|
|
110
242
|
.filter((r) => r.reason !== 'not_in_review');
|
|
111
243
|
const wouldLand = results.filter((r) => r.action === 'would_accept');
|
|
112
244
|
const blocked = results.filter((r) => r.action !== 'would_accept');
|
|
@@ -128,9 +260,18 @@ function showStatus(root, args) {
|
|
|
128
260
|
console.log('');
|
|
129
261
|
console.log(`autoland — certified work lands itself; you keep the irreversible calls`);
|
|
130
262
|
console.log('');
|
|
131
|
-
|
|
263
|
+
const policyOwner = String(policy?.enabled_by || 'unknown').trim() || 'unknown';
|
|
264
|
+
const policyText = enabled
|
|
265
|
+
? (policy.default
|
|
266
|
+
? `on by default for this workspace (no policy set; would accept as ${policyOwner}). set it: atris autoland on`
|
|
267
|
+
: `on (flipped by ${policyOwner}${policy.enabled_at ? ` on ${String(policy.enabled_at).slice(0, 10)}` : ''})`)
|
|
268
|
+
: 'off - everything waits for you';
|
|
269
|
+
console.log(` policy: ${policyText}`);
|
|
270
|
+
if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
|
|
132
271
|
console.log(` heartbeat: ${autoland.cronInstalled(root) ? 'running hourly' : 'not installed'}`);
|
|
133
272
|
if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
|
|
273
|
+
const reapTrouble = autoland.readState(root).last_reap_error;
|
|
274
|
+
if (reapTrouble) console.log(` cleanup trouble: daily sweep failed on ${reapTrouble.date} (${reapTrouble.error}) — run: atris land --reap`);
|
|
134
275
|
console.log('');
|
|
135
276
|
if (wouldLand.length > 0) {
|
|
136
277
|
console.log(` ready to land on their own: ${wouldLand.length}`);
|
|
@@ -163,6 +304,7 @@ function turnOn(root, args) {
|
|
|
163
304
|
const to = flag(args, '--to', '');
|
|
164
305
|
const digestHour = Number(flag(args, '--digest-hour', autoland.DEFAULT_DIGEST_HOUR));
|
|
165
306
|
const alarmHours = Number(flag(args, '--alarm-hours', autoland.DEFAULT_ALARM_HOURS));
|
|
307
|
+
const acceptAll = args.includes('--all');
|
|
166
308
|
const policy = autoland.writePolicy(root, {
|
|
167
309
|
enabled: true,
|
|
168
310
|
enabled_by: owner,
|
|
@@ -171,11 +313,13 @@ function turnOn(root, args) {
|
|
|
171
313
|
digest_hour: Number.isFinite(digestHour) ? digestHour : autoland.DEFAULT_DIGEST_HOUR,
|
|
172
314
|
alarm_hours: Number.isFinite(alarmHours) && alarmHours > 0 ? alarmHours : autoland.DEFAULT_ALARM_HOURS,
|
|
173
315
|
strict_verify: !args.includes('--no-strict-verify'),
|
|
316
|
+
accept_all: acceptAll,
|
|
174
317
|
});
|
|
175
318
|
const cronOk = autoland.installCron(root);
|
|
176
319
|
console.log('');
|
|
177
320
|
console.log('autoland is on.');
|
|
178
|
-
console.log(`
|
|
321
|
+
if (acceptAll) console.log(` everything in review now lands itself, accepted as ${owner} — only the protected lanes wait.`);
|
|
322
|
+
else console.log(` certified, verified, reversible work now lands itself, accepted as ${owner}.`);
|
|
179
323
|
console.log(' protected lanes (money, deploys, security, customer, outward) still wait for you.');
|
|
180
324
|
console.log(` heartbeat: ${cronOk ? 'installed, runs hourly' : 'could not install cron — run atris autoland tick yourself'}`);
|
|
181
325
|
if (policy.imessage_to) {
|
|
@@ -209,6 +353,8 @@ function runDigest(root, args, { forceSend = false } = {}) {
|
|
|
209
353
|
landed: landSummarySafe(root),
|
|
210
354
|
project: projectName(root),
|
|
211
355
|
nextMoves: digestNextMoves(root),
|
|
356
|
+
acceptAll: Boolean(policy.accept_all),
|
|
357
|
+
reapError: autoland.readState(root).last_reap_error?.error || null,
|
|
212
358
|
});
|
|
213
359
|
console.log(text);
|
|
214
360
|
// the full story: what each piece actually was, in its own words
|
|
@@ -239,6 +385,15 @@ function runDigest(root, args, { forceSend = false } = {}) {
|
|
|
239
385
|
return 0;
|
|
240
386
|
}
|
|
241
387
|
|
|
388
|
+
function pidAlive(pid) {
|
|
389
|
+
try {
|
|
390
|
+
process.kill(Number(pid), 0);
|
|
391
|
+
return true;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
242
397
|
function runTick(root, args) {
|
|
243
398
|
const json = args.includes('--json');
|
|
244
399
|
const policy = autoland.readPolicy(root);
|
|
@@ -249,28 +404,70 @@ function runTick(root, args) {
|
|
|
249
404
|
return 0;
|
|
250
405
|
}
|
|
251
406
|
|
|
407
|
+
// One live tick at a time: verify re-runs can take minutes each, and a
|
|
408
|
+
// second hourly cron firing into the same snapshot double-accepts,
|
|
409
|
+
// double-alarms, and double-digests. A stale lock (dead pid or >55min)
|
|
410
|
+
// never wedges the loop.
|
|
411
|
+
const lockPath = path.join(root, '.atris', 'state', 'autoland.tick.lock');
|
|
412
|
+
try {
|
|
413
|
+
const prev = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
414
|
+
const fresh = Date.now() - Number(prev.at || 0) < 55 * 60 * 1000;
|
|
415
|
+
if (fresh && pidAlive(prev.pid)) {
|
|
416
|
+
receipt.skipped_reason = 'tick_already_running';
|
|
417
|
+
if (json) console.log(JSON.stringify(receipt));
|
|
418
|
+
else console.log('autoland tick: another tick is still running; skipped.');
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
} catch {}
|
|
422
|
+
try {
|
|
423
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
424
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
425
|
+
} catch {}
|
|
426
|
+
try {
|
|
427
|
+
return runTickBody(root, { json, policy, receipt });
|
|
428
|
+
} finally {
|
|
429
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function runTickBody(root, { json, policy, receipt }) {
|
|
434
|
+
|
|
252
435
|
// 1. certify what has executable proof — re-run the runnable check named in
|
|
253
436
|
// each Review proof as a second actor. Without this the tick only lands rows
|
|
254
437
|
// some always-on mission happened to certify, and everything else waits on a
|
|
255
438
|
// human who never needed to look. Denied lanes and check-less proofs still wait.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
receipt.certify_error = certify.stderr.slice(0, 200) || 'certify-verified output unreadable';
|
|
264
|
-
}
|
|
439
|
+
const certify = runOwnCli(root, ['task', 'certify-verified', '--json']);
|
|
440
|
+
try {
|
|
441
|
+
const parsed = JSON.parse(certify.stdout);
|
|
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';
|
|
265
446
|
}
|
|
266
447
|
|
|
267
|
-
// 2. land what is eligible — the policy is the standing authorization
|
|
268
|
-
|
|
269
|
-
|
|
448
|
+
// 2. land what is eligible — the policy is the standing authorization.
|
|
449
|
+
// No hardcoded --limit here: a fixed low cap (this used to be 12) silently
|
|
450
|
+
// undercounts a real backlog every single tick — 12/hour forever even with
|
|
451
|
+
// 78 certified rows waiting. Let `atris task auto-accept-certified` apply
|
|
452
|
+
// its own default (12 without --all, a high safety cap under --all) so a
|
|
453
|
+
// policy with accept_all:true actually drains the full certified set.
|
|
454
|
+
const cliArgs = ['task', 'auto-accept-certified', '--json'];
|
|
455
|
+
if (policy.accept_all) cliArgs.push('--all');
|
|
456
|
+
else if (policy.strict_verify === false) cliArgs.push('--no-strict-verify');
|
|
270
457
|
const accept = runOwnCli(root, cliArgs);
|
|
271
458
|
try {
|
|
272
459
|
const parsed = JSON.parse(accept.stdout);
|
|
460
|
+
// A refused sweep (ok:false — a guard tripped, policy race) carries no
|
|
461
|
+
// summary fields. Name the reason instead of leaving nulls that read as
|
|
462
|
+
// "no work": a blind heartbeat must say WHY it is blind.
|
|
463
|
+
if (parsed.ok === false) {
|
|
464
|
+
receipt.accept_error = String(parsed.reason || 'auto-accept refused');
|
|
465
|
+
}
|
|
273
466
|
receipt.landed = (parsed.results || []).filter((r) => r.action === 'accepted').map((r) => r.ref);
|
|
467
|
+
receipt.certified = parsed.certified ?? null;
|
|
468
|
+
receipt.scanned = parsed.scanned ?? null;
|
|
469
|
+
receipt.skipped = parsed.skipped ?? null;
|
|
470
|
+
receipt.undercounted = Boolean(parsed.undercounted);
|
|
274
471
|
} catch (err) {
|
|
275
472
|
receipt.accept_error = accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
|
|
276
473
|
}
|
|
@@ -306,6 +503,42 @@ function runTick(root, args) {
|
|
|
306
503
|
}
|
|
307
504
|
}
|
|
308
505
|
|
|
506
|
+
// 3b. janitor, every tick: a mission left paused past 48h and a worktree
|
|
507
|
+
// already merged into base are the same rot pattern as an unlanded branch —
|
|
508
|
+
// left alone they accumulate until a human runs `mission stop` and
|
|
509
|
+
// `worktree cleanup --apply` by hand, which is exactly the chore autoland
|
|
510
|
+
// exists to remove. Off switch: policy.janitor === false (default on, the
|
|
511
|
+
// same absent-means-on convention as live_updates/strict_verify). Counts
|
|
512
|
+
// accumulate in state so the daily digest reports the day's total, not
|
|
513
|
+
// just the last tick's.
|
|
514
|
+
receipt.missions_stopped = 0;
|
|
515
|
+
receipt.worktrees_reaped = 0;
|
|
516
|
+
if (policy.janitor !== false) {
|
|
517
|
+
try {
|
|
518
|
+
const { reapPausedMissions } = require('./mission');
|
|
519
|
+
const stopped = reapPausedMissions(root);
|
|
520
|
+
receipt.missions_stopped = stopped.length;
|
|
521
|
+
if (stopped.length > 0) receipt.missions_stopped_refs = stopped.map((m) => m.id);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
receipt.janitor_mission_error = String((err && err.message) || err).slice(0, 200);
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const { cleanupWorktrees } = require('./worktree');
|
|
527
|
+
const cleaned = cleanupWorktrees({ root, apply: true });
|
|
528
|
+
receipt.worktrees_reaped = cleaned.removed.length;
|
|
529
|
+
if (cleaned.removed.length > 0) receipt.worktrees_reaped_paths = cleaned.removed.map((w) => w.path);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
receipt.janitor_worktree_error = String((err && err.message) || err).slice(0, 200);
|
|
532
|
+
}
|
|
533
|
+
if (receipt.missions_stopped || receipt.worktrees_reaped) {
|
|
534
|
+
const tally = state.janitor && typeof state.janitor === 'object' ? state.janitor : {};
|
|
535
|
+
state.janitor = {
|
|
536
|
+
missions_stopped: (Number(tally.missions_stopped) || 0) + receipt.missions_stopped,
|
|
537
|
+
worktrees_reaped: (Number(tally.worktrees_reaped) || 0) + receipt.worktrees_reaped,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
309
542
|
// 4. daily digest at the configured hour
|
|
310
543
|
const today = new Date().toISOString().slice(0, 10);
|
|
311
544
|
const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
|
|
@@ -316,6 +549,9 @@ function runTick(root, args) {
|
|
|
316
549
|
landed: landSummarySafe(root),
|
|
317
550
|
project: projectName(root),
|
|
318
551
|
nextMoves: digestNextMoves(root),
|
|
552
|
+
acceptAll: Boolean(policy.accept_all),
|
|
553
|
+
reapError: state.last_reap_error?.error || null,
|
|
554
|
+
janitor: state.janitor || null,
|
|
319
555
|
});
|
|
320
556
|
if (policy.imessage_to) {
|
|
321
557
|
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
@@ -323,6 +559,7 @@ function runTick(root, args) {
|
|
|
323
559
|
}
|
|
324
560
|
receipt.digest_text = text;
|
|
325
561
|
state.last_digest_date = today;
|
|
562
|
+
delete state.janitor;
|
|
326
563
|
|
|
327
564
|
// 5. once a day, keep the receipt shelf lean: compress old run receipts
|
|
328
565
|
// into the manifest and drop unreferenced clutter, newest 200 kept.
|
|
@@ -334,11 +571,58 @@ function runTick(root, args) {
|
|
|
334
571
|
receipt.receipts_pruned = null;
|
|
335
572
|
}
|
|
336
573
|
}
|
|
574
|
+
// 6. once a day, drain the landing itself: back up (bundle + patches into
|
|
575
|
+
// .atris/salvage/) then clear branches already landed or past TTL, and
|
|
576
|
+
// their worktrees. Local-only — remote branches may back open PRs, and
|
|
577
|
+
// closing those is a human call. Its own date gate, not the digest hour,
|
|
578
|
+
// so a machine asleep at digest time still drains on its next tick.
|
|
579
|
+
// Without this the board grows until a human runs `atris land --reap`,
|
|
580
|
+
// which is exactly the chore autoland exists to remove.
|
|
581
|
+
if (state.last_reap_date !== today) {
|
|
582
|
+
try {
|
|
583
|
+
const { reap } = require('./land');
|
|
584
|
+
const reaped = reap(root, { remote: false, includeDetached: false });
|
|
585
|
+
receipt.reaped = {
|
|
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);
|
|
594
|
+
}
|
|
595
|
+
// Missions rot the same way branches do: paused/planning/ready and
|
|
596
|
+
// untouched for a week means abandoned. Age them out under the same
|
|
597
|
+
// daily gate so `mission list` shows work, not archaeology.
|
|
598
|
+
try {
|
|
599
|
+
const { expireStaleMissions } = require('./mission');
|
|
600
|
+
const expiredMissions = expireStaleMissions(root);
|
|
601
|
+
if (expiredMissions.length > 0) receipt.expired_missions = expiredMissions.length;
|
|
602
|
+
} catch (err) {
|
|
603
|
+
receipt.mission_expiry_error = String((err && err.message) || err).slice(0, 200);
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const missionVerify = verifyClosedTaskMissions(root, readProjection(root));
|
|
607
|
+
if (missionVerify.verified.length > 0) receipt.verified_missions = missionVerify.verified;
|
|
608
|
+
if (missionVerify.errors.length > 0) receipt.mission_verify_errors = missionVerify.errors;
|
|
609
|
+
} catch (err) {
|
|
610
|
+
receipt.mission_verify_errors = [{ error: String((err && err.message) || err).slice(0, 200) }];
|
|
611
|
+
}
|
|
612
|
+
state.last_reap_date = today;
|
|
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;
|
|
618
|
+
}
|
|
337
619
|
autoland.writeState(root, state);
|
|
338
620
|
|
|
339
621
|
if (json) console.log(JSON.stringify(receipt));
|
|
340
622
|
else {
|
|
341
|
-
|
|
623
|
+
const reapNote = receipt.reaped ? `, reaped ${receipt.reaped.branches} landed/overdue branches` : '';
|
|
624
|
+
const janitorNote = `, janitor stopped ${receipt.missions_stopped} mission${receipt.missions_stopped === 1 ? '' : 's'} + reaped ${receipt.worktrees_reaped} worktree${receipt.worktrees_reaped === 1 ? '' : 's'}`;
|
|
625
|
+
console.log(`autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${receipt.digest_sent ? 'sent' : 'not due'}${reapNote}${janitorNote}`);
|
|
342
626
|
}
|
|
343
627
|
return 0;
|
|
344
628
|
}
|
|
@@ -355,6 +639,9 @@ function showHelp() {
|
|
|
355
639
|
console.log(' atris autoland on [--to <phone>] flip it on: hourly heartbeat, daily');
|
|
356
640
|
console.log(' message, ping when something waits');
|
|
357
641
|
console.log(' on you past 24h');
|
|
642
|
+
console.log(' atris autoland on --all lower the bar: everything lands except');
|
|
643
|
+
console.log(' the protected lanes; a failing recorded');
|
|
644
|
+
console.log(' check still blocks');
|
|
358
645
|
console.log(' atris autoland off back to approving every item');
|
|
359
646
|
console.log(' atris autoland digest [--send] the daily message, now');
|
|
360
647
|
console.log(' atris autoland tick [--json] one heartbeat (what the hourly cron runs)');
|
|
@@ -382,4 +669,11 @@ function autolandCommand(args = []) {
|
|
|
382
669
|
return 1;
|
|
383
670
|
}
|
|
384
671
|
|
|
385
|
-
module.exports = {
|
|
672
|
+
module.exports = {
|
|
673
|
+
autolandCommand,
|
|
674
|
+
attachedTasksForMission,
|
|
675
|
+
missionReadyForClosedTaskVerify,
|
|
676
|
+
operatorReady,
|
|
677
|
+
hasAgentJargon,
|
|
678
|
+
verifyClosedTaskMissions,
|
|
679
|
+
};
|
package/commands/business.js
CHANGED
|
@@ -28,6 +28,42 @@ function isHelpToken(arg) {
|
|
|
28
28
|
return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const BUSINESS_CREATE_USAGE = 'Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]';
|
|
32
|
+
|
|
33
|
+
function isAccidentalHelpBusiness(value) {
|
|
34
|
+
const raw = String(value || '').trim();
|
|
35
|
+
if (!raw) return false;
|
|
36
|
+
if (isHelpToken(raw)) return true;
|
|
37
|
+
const normalized = slugify(raw);
|
|
38
|
+
if (normalized === 'help') return true;
|
|
39
|
+
if (/^help-\d+$/.test(normalized)) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateBusinessCreateName(name) {
|
|
44
|
+
if (!name || !String(name).trim()) {
|
|
45
|
+
return { ok: false, usage: BUSINESS_CREATE_USAGE };
|
|
46
|
+
}
|
|
47
|
+
if (isHelpToken(name)) {
|
|
48
|
+
return { ok: false, usage: BUSINESS_CREATE_USAGE };
|
|
49
|
+
}
|
|
50
|
+
if (String(name).startsWith('-')) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
usage: BUSINESS_CREATE_USAGE,
|
|
54
|
+
detail: `Refusing to create a business named "${name}" — looks like a flag, not a name.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (isAccidentalHelpBusiness(name)) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
usage: BUSINESS_CREATE_USAGE,
|
|
61
|
+
detail: `Refusing to create a business named "${name}" — looks like accidental help output, not a real business.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
function loadBusinesses() {
|
|
32
68
|
const p = getBusinessConfigPath();
|
|
33
69
|
if (!fs.existsSync(p)) return {};
|
|
@@ -308,14 +344,46 @@ function analyzeBusinessDoctor({ cache = {}, cloudBusinesses = [], folderBinding
|
|
|
308
344
|
}
|
|
309
345
|
}
|
|
310
346
|
|
|
347
|
+
const cacheRemovals = [];
|
|
348
|
+
for (const business of cloudBusinesses || []) {
|
|
349
|
+
if (!isAccidentalHelpBusiness(business?.name) && !isAccidentalHelpBusiness(business?.slug)) continue;
|
|
350
|
+
const label = business.slug || business.name || business.id;
|
|
351
|
+
issues.push({
|
|
352
|
+
level: 'warn',
|
|
353
|
+
code: 'accidental-help-business',
|
|
354
|
+
subject: label,
|
|
355
|
+
message: `${label} looks like an accidental help business (April 2026 ghost rows)`,
|
|
356
|
+
fixable: true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const [key, entry] of Object.entries(cache || {})) {
|
|
361
|
+
const active = findActiveMatch(key, entry);
|
|
362
|
+
const accidental = isAccidentalHelpBusiness(key)
|
|
363
|
+
|| isAccidentalHelpBusiness(entry?.name)
|
|
364
|
+
|| isAccidentalHelpBusiness(entry?.slug)
|
|
365
|
+
|| (active && (isAccidentalHelpBusiness(active.name) || isAccidentalHelpBusiness(active.slug)));
|
|
366
|
+
if (!accidental) continue;
|
|
367
|
+
issues.push({
|
|
368
|
+
level: 'warn',
|
|
369
|
+
code: 'accidental-help-cache',
|
|
370
|
+
subject: key,
|
|
371
|
+
message: `${key} is cached for an accidental help business`,
|
|
372
|
+
fixable: true,
|
|
373
|
+
});
|
|
374
|
+
if (!cacheRemovals.includes(key)) cacheRemovals.push(key);
|
|
375
|
+
}
|
|
376
|
+
|
|
311
377
|
return {
|
|
312
378
|
issues,
|
|
313
379
|
cacheUpdates,
|
|
380
|
+
cacheRemovals,
|
|
314
381
|
stats: {
|
|
315
382
|
cache_entries: Object.keys(cache || {}).length,
|
|
316
383
|
cloud_active: cloudBusinesses.length,
|
|
317
384
|
folders: folderBindings.length,
|
|
318
385
|
fixable_cache_entries: Object.keys(cacheUpdates).length,
|
|
386
|
+
accidental_help_cache_removals: cacheRemovals.length,
|
|
319
387
|
},
|
|
320
388
|
};
|
|
321
389
|
}
|
|
@@ -2191,11 +2259,20 @@ async function businessDoctor(...args) {
|
|
|
2191
2259
|
});
|
|
2192
2260
|
|
|
2193
2261
|
const cacheUpdateKeys = Object.keys(analysis.cacheUpdates);
|
|
2262
|
+
const cacheRemovalKeys = analysis.cacheRemovals || [];
|
|
2194
2263
|
let fixed = [];
|
|
2195
|
-
if (options.fix && cacheUpdateKeys.length > 0) {
|
|
2196
|
-
|
|
2264
|
+
if (options.fix && (cacheUpdateKeys.length > 0 || cacheRemovalKeys.length > 0)) {
|
|
2265
|
+
if (cacheUpdateKeys.length > 0) {
|
|
2266
|
+
cache = { ...cache, ...analysis.cacheUpdates };
|
|
2267
|
+
}
|
|
2268
|
+
for (const key of cacheRemovalKeys) {
|
|
2269
|
+
delete cache[key];
|
|
2270
|
+
fixed.push(`removed:${key}`);
|
|
2271
|
+
}
|
|
2272
|
+
if (cacheUpdateKeys.length > 0) {
|
|
2273
|
+
fixed.push(...cacheUpdateKeys);
|
|
2274
|
+
}
|
|
2197
2275
|
saveBusinesses(cache);
|
|
2198
|
-
fixed = cacheUpdateKeys;
|
|
2199
2276
|
analysis = analyzeBusinessDoctor({
|
|
2200
2277
|
cache,
|
|
2201
2278
|
cloudBusinesses: listResult.data,
|
|
@@ -2237,11 +2314,10 @@ async function businessDoctor(...args) {
|
|
|
2237
2314
|
}
|
|
2238
2315
|
|
|
2239
2316
|
async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
}
|
|
2317
|
+
const validation = validateBusinessCreateName(name);
|
|
2318
|
+
if (!validation.ok) {
|
|
2319
|
+
console.error(validation.usage);
|
|
2320
|
+
if (validation.detail) console.error(`\n ${validation.detail}`);
|
|
2245
2321
|
process.exit(1);
|
|
2246
2322
|
}
|
|
2247
2323
|
|
|
@@ -2897,6 +2973,11 @@ async function businessCommand(subcommand, ...args) {
|
|
|
2897
2973
|
printBusinessHelp();
|
|
2898
2974
|
return;
|
|
2899
2975
|
}
|
|
2976
|
+
const nameTakingSubcommands = new Set(['create', 'new', 'init', 'workspace']);
|
|
2977
|
+
if (nameTakingSubcommands.has(subcommand) && args.some(isHelpToken)) {
|
|
2978
|
+
printBusinessHelp();
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2900
2981
|
if (args.length > 0 && isHelpToken(args[0]) && subcommand !== 'doctor') {
|
|
2901
2982
|
printBusinessHelp();
|
|
2902
2983
|
return;
|
|
@@ -2999,6 +3080,8 @@ module.exports = {
|
|
|
2999
3080
|
saveBusinesses,
|
|
3000
3081
|
getBusinessConfigPath,
|
|
3001
3082
|
businessMatchesSlug,
|
|
3083
|
+
isAccidentalHelpBusiness,
|
|
3084
|
+
validateBusinessCreateName,
|
|
3002
3085
|
analyzeBusinessDoctor,
|
|
3003
3086
|
readBusinessFolderBindings,
|
|
3004
3087
|
createCanonicalBusinessWorkspace,
|
package/commands/codex-goal.js
CHANGED
|
@@ -99,11 +99,35 @@ function chmodPrivate(filePath) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function
|
|
102
|
+
function runSqliteOnce(dbPath, sql, readonly) {
|
|
103
103
|
const sqliteArgs = [];
|
|
104
104
|
if (readonly) sqliteArgs.push('-readonly');
|
|
105
105
|
sqliteArgs.push('-json', dbPath, sql);
|
|
106
|
-
|
|
106
|
+
return spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function runSqliteJson(dbPath, sql, { readonly = true } = {}) {
|
|
110
|
+
let result = runSqliteOnce(dbPath, sql, readonly);
|
|
111
|
+
// Codex DBs are WAL-mode. `sqlite3 -readonly` cannot open a WAL db unless a
|
|
112
|
+
// writer already holds the -shm/-wal sidecars open (error 14 with no Codex
|
|
113
|
+
// running). Fall back to reading a private snapshot copy of db+wal.
|
|
114
|
+
const walLocked =
|
|
115
|
+
readonly &&
|
|
116
|
+
result.status !== 0 &&
|
|
117
|
+
/unable to open database file/i.test(String(result.stderr || result.stdout || ''));
|
|
118
|
+
if (walLocked) {
|
|
119
|
+
const snapDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-codex-goal-'));
|
|
120
|
+
const snapDb = path.join(snapDir, path.basename(dbPath));
|
|
121
|
+
try {
|
|
122
|
+
fs.copyFileSync(dbPath, snapDb);
|
|
123
|
+
for (const ext of ['-wal', '-shm']) {
|
|
124
|
+
if (fs.existsSync(dbPath + ext)) fs.copyFileSync(dbPath + ext, snapDb + ext);
|
|
125
|
+
}
|
|
126
|
+
result = runSqliteOnce(snapDb, sql, false);
|
|
127
|
+
} finally {
|
|
128
|
+
fs.rmSync(snapDir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
107
131
|
if (result.error) throw new Error(`sqlite3 failed: ${result.error.message}`);
|
|
108
132
|
if (result.status !== 0) {
|
|
109
133
|
const detail = (result.stderr || result.stdout || '').trim();
|