atris 3.78.0 → 3.80.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/README.md +7 -0
- package/atris/atris.md +66 -167
- package/atris/skills/security-audit/SKILL.md +77 -0
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris.md +66 -167
- package/bin/atris.js +6 -0
- package/commands/brief.js +8 -2
- package/commands/business.js +44 -1984
- package/commands/gm.js +3 -0
- package/commands/member.js +189 -966
- package/commands/mission.js +229 -1409
- package/commands/pack.js +112 -1476
- package/commands/rating.js +2 -0
- package/commands/security-review.js +125 -0
- package/commands/task.js +137 -1359
- package/lib/business-core.js +2022 -0
- package/lib/engine-ask.js +1 -0
- package/lib/fleet.js +10 -0
- package/lib/handoff-wake.js +8 -6
- package/lib/held-autosweep.js +559 -0
- package/lib/held-pull-requests.js +42 -15
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +21 -2
- package/lib/member-board.js +12 -10
- package/lib/member-core.js +1014 -0
- package/lib/member-up.js +46 -3
- package/lib/mission-core.js +1559 -0
- package/lib/pack-core.js +1588 -0
- package/lib/security-audit-classes.js +300 -0
- package/lib/security-audit.js +1519 -0
- package/lib/task-core.js +1412 -0
- package/lib/task-db.js +70 -27
- package/lib/task-decision.js +66 -0
- package/lib/task-explanation.js +20 -0
- package/lib/yes-lock.js +1 -1
- package/package.json +1 -1
|
@@ -234,15 +234,16 @@ const GIT_REPOSITORY_VARIABLES = [
|
|
|
234
234
|
|
|
235
235
|
// Which repository a workspace came from: the owner and name its origin remote points at, asked of
|
|
236
236
|
// git in the workspace's own folder. Null when git cannot say (no folder, no origin) or the remote
|
|
237
|
-
// does not read as a GitHub repository.
|
|
238
|
-
|
|
237
|
+
// does not read as a GitHub repository. `run` and `timeoutMs` (10 seconds unless told) are for the
|
|
238
|
+
// caller that holds git to a limit of its own, and for tests.
|
|
239
|
+
function originRepoOf(root, { run = spawnSync, timeoutMs = 10000 } = {}) {
|
|
239
240
|
if (!root) return null;
|
|
240
241
|
const env = { ...process.env };
|
|
241
242
|
for (const key of GIT_REPOSITORY_VARIABLES) delete env[key];
|
|
242
243
|
const done = run('git', ['-C', String(root), 'remote', 'get-url', 'origin'], {
|
|
243
244
|
encoding: 'utf8',
|
|
244
245
|
env,
|
|
245
|
-
timeout:
|
|
246
|
+
timeout: timeoutMs,
|
|
246
247
|
killSignal: 'SIGKILL',
|
|
247
248
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
248
249
|
}) || {};
|
|
@@ -289,6 +290,14 @@ function leftAloneDetail(reason) {
|
|
|
289
290
|
return 'it changed while GitHub was being asked, so it was left alone';
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
// The jobs among `rows` that wait on a pull request, oldest first (by when they were made, then by row id).
|
|
294
|
+
// The order is the order the sweep goes through them in.
|
|
295
|
+
function heldJobsAmong(taskStore, rows) {
|
|
296
|
+
return rows
|
|
297
|
+
.filter((row) => row.status === 'review' && taskStore.heldPullRequestOf(row))
|
|
298
|
+
.sort((a, b) => (Number(a.created_at || 0) - Number(b.created_at || 0)) || String(a.id).localeCompare(String(b.id)));
|
|
299
|
+
}
|
|
300
|
+
|
|
292
301
|
// Check every held job against GitHub and, unless dryRun, write down what happened.
|
|
293
302
|
//
|
|
294
303
|
// taskStore lib/task-db
|
|
@@ -298,6 +307,21 @@ function leftAloneDetail(reason) {
|
|
|
298
307
|
// ask how to ask GitHub about a link (askGitHub); asked once per link per run
|
|
299
308
|
// askBranch how to ask GitHub for a repository's default branch (askDefaultBranch); asked
|
|
300
309
|
// once per repository per run, and only for a pull request that was merged
|
|
310
|
+
// askOrigin how to read the repository a workspace came from (originRepoOf); asked once per
|
|
311
|
+
// workspace per run, and only for a pull request that was merged or closed
|
|
312
|
+
// only a row id: sweep that one job and none of the others (the automatic run goes job
|
|
313
|
+
// by job, to say each result as it is made)
|
|
314
|
+
// listing task rows already read. Given, this call lists and numbers nothing itself, and `only`
|
|
315
|
+
// still picks from it. A job in it is named by its number if the row has one (display_id or
|
|
316
|
+
// legacy_ref) and by its id if it has not. The automatic run reads the workspace once for the
|
|
317
|
+
// whole run and hands each call its job, because it prints no task number and so has none to
|
|
318
|
+
// make. Left out, as the manual sweep leaves it, this call lists and numbers the workspace
|
|
319
|
+
// itself.
|
|
320
|
+
// nameOf how a refusal names the other job that has the pull request, given its row id. Left
|
|
321
|
+
// out, the name is the one that job has in its own workspace's numbering, which lists
|
|
322
|
+
// that workspace's tasks and so can start git when checkouts are linked to it. The
|
|
323
|
+
// automatic run, which says nothing of a refusal and keeps to a time limit, gives the
|
|
324
|
+
// row id itself.
|
|
301
325
|
//
|
|
302
326
|
// Returns { ok, action, dry_run, scope, actor, outage, summary, results }. ok is false only on
|
|
303
327
|
// an outage. Each result is { task_id, ref, title, builder, workspace_root, pull_request,
|
|
@@ -305,15 +329,18 @@ function leftAloneDetail(reason) {
|
|
|
305
329
|
// unknown or not_asked, and action is what was done: landed, released, would_land,
|
|
306
330
|
// would_release, none, left_alone, or refused (the pull request is not this job's build: nothing
|
|
307
331
|
// was written, and detail is the plain line saying why).
|
|
308
|
-
function sweepHeldJobs(taskStore, db, { workspaceRoot = null, actor, dryRun = false, ask = askGitHub, askBranch = askDefaultBranch } = {}) {
|
|
332
|
+
function sweepHeldJobs(taskStore, db, { workspaceRoot = null, actor, dryRun = false, ask = askGitHub, askBranch = askDefaultBranch, askOrigin = originRepoOf, only = null, nameOf = null, listing = null } = {}) {
|
|
309
333
|
if (!actor) throw new Error('actor required');
|
|
310
334
|
const scope = workspaceRoot ? 'workspace' : 'everywhere';
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
335
|
+
let named;
|
|
336
|
+
if (listing) {
|
|
337
|
+
named = heldJobsAmong(taskStore, listing).filter((row) => !only || row.id === only);
|
|
338
|
+
} else {
|
|
339
|
+
// No task list yet means nothing is held.
|
|
340
|
+
const inScope = db ? taskStore.listTasks(db, { workspaceRoot: workspaceRoot || null }) : [];
|
|
341
|
+
const held = heldJobsAmong(taskStore, inScope).filter((row) => !only || row.id === only);
|
|
342
|
+
named = taskStore.withTaskDisplayRefs(held, inScope);
|
|
343
|
+
}
|
|
317
344
|
|
|
318
345
|
// Each question goes to GitHub once per run, whatever the answer: a failure is not asked again.
|
|
319
346
|
const answers = new Map();
|
|
@@ -347,17 +374,17 @@ function sweepHeldJobs(taskStore, db, { workspaceRoot = null, actor, dryRun = fa
|
|
|
347
374
|
// A workspace's origin is read once per run, and only if a pull request needs it.
|
|
348
375
|
const origins = new Map();
|
|
349
376
|
const originOf = (root) => {
|
|
350
|
-
if (!origins.has(root)) origins.set(root,
|
|
377
|
+
if (!origins.has(root)) origins.set(root, askOrigin(root));
|
|
351
378
|
return origins.get(root);
|
|
352
379
|
};
|
|
353
380
|
|
|
354
|
-
// The name a person reads for a job, in its own workspace's numbering.
|
|
355
|
-
const refOfJob = (id) => {
|
|
381
|
+
// The name a person reads for a job, in its own workspace's numbering, unless the caller names jobs itself.
|
|
382
|
+
const refOfJob = nameOf || ((id) => {
|
|
356
383
|
const task = taskStore.getTask(db, id);
|
|
357
384
|
if (!task) return String(id);
|
|
358
385
|
const [shown] = taskStore.withTaskDisplayRefs([task], taskStore.listTasks(db, { workspaceRoot: task.workspace_root || null }));
|
|
359
386
|
return (shown && (shown.display_id || shown.legacy_ref || shown.id)) || String(id);
|
|
360
|
-
};
|
|
387
|
+
});
|
|
361
388
|
// Why this pull request is not this job's to settle, when another job has it.
|
|
362
389
|
const takenReason = (taken, answer) => (taken.how === 'landed'
|
|
363
390
|
? `pull request ${answer.number} in ${answer.repo} already landed with ${refOfJob(taken.id)}`
|
|
@@ -586,4 +613,4 @@ function describeHeldSweep(report) {
|
|
|
586
613
|
return lines;
|
|
587
614
|
}
|
|
588
615
|
|
|
589
|
-
module.exports = { ASK_TIMEOUT_MS, askGitHub, askDefaultBranch, branchNamesJob, originRepoOf, sweepHeldJobs, describeHeldSweep };
|
|
616
|
+
module.exports = { ASK_TIMEOUT_MS, askGitHub, askDefaultBranch, branchNamesJob, heldJobsAmong, originRepoOf, sweepHeldJobs, describeHeldSweep };
|
package/lib/known-commands.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'idea', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', '99', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'rating', 'sync-checkout', 'zen',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'sentinel', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'rating', 'sync-checkout', 'zen',
|
|
7
7
|
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'google', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi', 'pay', 'trust'];
|
|
9
9
|
|
package/lib/member-autoland.js
CHANGED
|
@@ -176,7 +176,7 @@ function latestRunReceipt(missionId, cwd) {
|
|
|
176
176
|
// mission's own state and no commit made after it. Every tick in that run
|
|
177
177
|
// must have passed its check and a verdict from a reviewer engine other than
|
|
178
178
|
// the member and every engine that did the work.
|
|
179
|
-
function reviewHold(mission, workerEngine, cwd, pin = {}) {
|
|
179
|
+
function reviewHold(mission, workerEngine, cwd, pin = {}, doneLooksLikes = []) {
|
|
180
180
|
const receipt = latestRunReceipt(mission.id, cwd);
|
|
181
181
|
if (!receipt) return 'no run receipt for this mission could be read';
|
|
182
182
|
const result = receipt.result && typeof receipt.result === 'object' ? receipt.result : {};
|
|
@@ -220,6 +220,18 @@ function reviewHold(mission, workerEngine, cwd, pin = {}) {
|
|
|
220
220
|
if (!reviewer || self.has(reviewer)) return 'the latest run was reviewed by the same engine that did the work';
|
|
221
221
|
}
|
|
222
222
|
if (lastTick.reviewed_git_tree !== head.stdout) return 'the review did not see the code that is checked out now';
|
|
223
|
+
const doneLines = [
|
|
224
|
+
...(Array.isArray(doneLooksLikes) ? doneLooksLikes : []),
|
|
225
|
+
mission.done_looks_like,
|
|
226
|
+
].map((line) => String(line || '').trim()).filter(Boolean);
|
|
227
|
+
if (doneLines.length) {
|
|
228
|
+
const { missingDoneLooksLikeClaim } = require('../commands/mission');
|
|
229
|
+
const reviewOutput = String(lastTick.referee_result?.output || '');
|
|
230
|
+
for (const doneLine of doneLines) {
|
|
231
|
+
const missing = missingDoneLooksLikeClaim(doneLine, { cwd, reviewOutput });
|
|
232
|
+
if (missing) return `missing: ${missing}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
223
235
|
pin.tree = head.stdout;
|
|
224
236
|
return null;
|
|
225
237
|
}
|
|
@@ -459,6 +471,7 @@ function autolandMemberRun({ mission, cwd, summary = '', workerEngine = '', load
|
|
|
459
471
|
if (missionLane) return held('protected', `this mission sits in a protected area (${missionLane})`);
|
|
460
472
|
const taskIds = linkedTaskIds(mission);
|
|
461
473
|
if (!taskIds) return held('protected', 'the mission\'s linked tasks are not a clean list of ids');
|
|
474
|
+
const doneLooksLikes = [];
|
|
462
475
|
for (const id of taskIds) {
|
|
463
476
|
let task = null;
|
|
464
477
|
try {
|
|
@@ -475,12 +488,18 @@ function autolandMemberRun({ mission, cwd, summary = '', workerEngine = '', load
|
|
|
475
488
|
if (approvals.some((value) => ['revise', 'revised', 'rejected'].includes(String(value || '').trim().toLowerCase()))) {
|
|
476
489
|
return held('approval', 'a linked task was turned down; it needs fresh proof before it can go to github');
|
|
477
490
|
}
|
|
491
|
+
try {
|
|
492
|
+
const explanation = require('./task-explanation').taskExplanation(task);
|
|
493
|
+
if (explanation.sources?.done_looks_like === 'explicit' && explanation.done_looks_like) {
|
|
494
|
+
doneLooksLikes.push(explanation.done_looks_like);
|
|
495
|
+
}
|
|
496
|
+
} catch {}
|
|
478
497
|
}
|
|
479
498
|
|
|
480
499
|
const unsaved = unsavedHold(cwd);
|
|
481
500
|
if (unsaved) return held('unsaved', unsaved);
|
|
482
501
|
const pin = {};
|
|
483
|
-
const reviewGap = reviewHold(mission, workerEngine, cwd, pin);
|
|
502
|
+
const reviewGap = reviewHold(mission, workerEngine, cwd, pin, doneLooksLikes);
|
|
484
503
|
if (reviewGap) return held('review', reviewGap);
|
|
485
504
|
// Pin the commit the review passed on. Only this commit (or a map-only
|
|
486
505
|
// merge built on it) is pushed and merged, so a commit made after this
|
package/lib/member-board.js
CHANGED
|
@@ -261,9 +261,9 @@ function progressEvent(member, raw) {
|
|
|
261
261
|
// so a line half written before the stream started still arrives once done;
|
|
262
262
|
// files that appear later are read from the top. scan() is sync, so watch
|
|
263
263
|
// and poll can both call it without racing.
|
|
264
|
-
function afterLastNewline(file) {
|
|
264
|
+
function afterLastNewline(file, onError = () => {}) {
|
|
265
265
|
let fd;
|
|
266
|
-
try { fd = fs.openSync(file, 'r'); } catch { return 0; }
|
|
266
|
+
try { fd = fs.openSync(file, 'r'); } catch (error) { onError(`open ${file}`, error); return 0; }
|
|
267
267
|
try {
|
|
268
268
|
const buf = Buffer.alloc(64 * 1024);
|
|
269
269
|
for (let end = fs.fstatSync(fd).size; end > 0;) {
|
|
@@ -274,14 +274,16 @@ function afterLastNewline(file) {
|
|
|
274
274
|
end = start;
|
|
275
275
|
}
|
|
276
276
|
return 0;
|
|
277
|
-
} catch { return 0; } finally { fs.closeSync(fd); }
|
|
277
|
+
} catch (error) { onError(`read ${file}`, error); return 0; } finally { fs.closeSync(fd); }
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = false } = {}) {
|
|
280
|
+
function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = false, onError = () => {} } = {}) {
|
|
281
281
|
const offsets = new Map();
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
282
|
+
let directorySeen = false;
|
|
283
|
+
const report = (context, error) => { if (error.code !== 'ENOENT' || directorySeen) onError(context, error); };
|
|
284
|
+
const sizeOf = (file) => { try { return fs.statSync(file).size; } catch (error) { report(`stat ${file}`, error); return null; } };
|
|
285
|
+
const list = () => { try { const files = fs.readdirSync(dir); directorySeen = true; return files.filter((f) => f.endsWith('.log')); } catch (error) { report(`list ${dir}`, error); return []; } };
|
|
286
|
+
for (const f of list()) offsets.set(f, afterLastNewline(path.join(dir, f), report));
|
|
285
287
|
|
|
286
288
|
function readNew(f, events) {
|
|
287
289
|
const file = path.join(dir, f);
|
|
@@ -297,7 +299,7 @@ function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = f
|
|
|
297
299
|
buf = Buffer.alloc(size - from);
|
|
298
300
|
buf = buf.subarray(0, fs.readSync(fd, buf, 0, buf.length, from));
|
|
299
301
|
} finally { fs.closeSync(fd); }
|
|
300
|
-
} catch { return; }
|
|
302
|
+
} catch (error) { report(`read ${file}`, error); return; }
|
|
301
303
|
// Stop at the last newline; a half-written tail waits for the next scan.
|
|
302
304
|
const end = buf.lastIndexOf(0x0a);
|
|
303
305
|
if (end < 0) { offsets.set(f, from); return; }
|
|
@@ -316,8 +318,8 @@ function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = f
|
|
|
316
318
|
if (!watcher) {
|
|
317
319
|
try {
|
|
318
320
|
watcher = fs.watch(dir, () => scan());
|
|
319
|
-
watcher.on('error', () => { try { watcher.close(); } catch {} watcher = null; });
|
|
320
|
-
} catch { watcher = null; }
|
|
321
|
+
watcher.on('error', (error) => { report(`watch ${dir}`, error); try { watcher.close(); } catch (closeError) { report(`close watcher ${dir}`, closeError); } watcher = null; });
|
|
322
|
+
} catch (error) { report(`watch ${dir}`, error); watcher = null; }
|
|
321
323
|
}
|
|
322
324
|
const events = [];
|
|
323
325
|
for (const f of list()) readNew(f, events);
|