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
package/lib/task-db.js
CHANGED
|
@@ -197,24 +197,39 @@ function getDbPath() {
|
|
|
197
197
|
return process.env.ATRIS_TASKS_DB || path.join(atrisHome(), 'tasks.db');
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
// How many times each setup statement of a short-wait connection is tried while another process holds
|
|
201
|
+
// the write lock: with the connection's own busy wait left at nothing, about a second in all.
|
|
202
|
+
const SHORT_WAIT_TRIES = 7;
|
|
203
|
+
|
|
204
|
+
// shortWait is for a caller that must not hang behind other writers (the automatic held-job sweep). It
|
|
205
|
+
// gets a connection of its own, never the cached one, that the caller closes. The connection's own busy
|
|
206
|
+
// wait stays at nothing, so a store that stays locked is given up on after about a second, with the busy
|
|
207
|
+
// error (see isBusyError) for the caller to tell apart, and the handle is closed.
|
|
208
|
+
function open(dbPath, { shortWait = false } = {}) {
|
|
201
209
|
const target = dbPath || getDbPath();
|
|
202
|
-
if (_cachedDb && _cachedPath === target) return _cachedDb;
|
|
210
|
+
if (!shortWait && _cachedDb && _cachedPath === target) return _cachedDb;
|
|
203
211
|
const dir = path.dirname(target);
|
|
204
212
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
205
213
|
const db = new DatabaseSync(target);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
const tries = shortWait ? SHORT_WAIT_TRIES : undefined;
|
|
215
|
+
try {
|
|
216
|
+
// Concurrency: WAL gives readers + one writer concurrency; busy_timeout
|
|
217
|
+
// makes contended writers wait instead of returning SQLITE_BUSY at the
|
|
218
|
+
// C library level. We additionally wrap the setup PRAGMAs + DDL in our
|
|
219
|
+
// own retry, under heavy spawn-storm contention, node:sqlite leaks
|
|
220
|
+
// SQLITE_BUSY past the busy_timeout for `db.exec()` calls.
|
|
221
|
+
withBusyRetry(() => db.exec('PRAGMA journal_mode = WAL'), tries);
|
|
222
|
+
if (!shortWait) withBusyRetry(() => db.exec('PRAGMA busy_timeout = 30000'));
|
|
223
|
+
withBusyRetry(() => db.exec('PRAGMA foreign_keys = ON'), tries);
|
|
224
|
+
withBusyRetry(() => db.exec(SCHEMA), tries);
|
|
225
|
+
// Schema version. Bump at every additive migration.
|
|
226
|
+
// Future migrations read this and apply diffs idempotently.
|
|
227
|
+
withBusyRetry(() => db.exec('PRAGMA user_version = 2'), tries);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
try { db.close(); } catch (_) {}
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
if (shortWait) return db;
|
|
218
233
|
_cachedDb = db;
|
|
219
234
|
_cachedPath = target;
|
|
220
235
|
return db;
|
|
@@ -476,21 +491,43 @@ function findUnfinishedTaskByTitle(db, { workspaceRoot: ws, title }) {
|
|
|
476
491
|
// A main checkout's view also covers its linked git worktrees, whose tasks keep
|
|
477
492
|
// the worktree as workspace_root so checks run where the work lives. Only a
|
|
478
493
|
// main checkout with linked worktrees spawns git; any error means exact match.
|
|
494
|
+
// A git that does not answer in time is ended outright (SIGKILL, because the
|
|
495
|
+
// polite stop can be ignored), so no git can hold a command for good.
|
|
496
|
+
const LINKED_WORKTREE_LOOKUP_MS = 5000;
|
|
479
497
|
const linkedWorktreeCache = new Map();
|
|
480
|
-
|
|
498
|
+
|
|
499
|
+
// { roots, failed }: the checkouts linked to ws, and whether git could not say (it
|
|
500
|
+
// did not answer within timeoutMs, could not be run, or exited with an error), in
|
|
501
|
+
// which case roots is empty. The task list ignores the failure and shows the
|
|
502
|
+
// workspace alone; a caller that must know asks here. A failure is remembered for the
|
|
503
|
+
// process, except one under a limit the caller chose that is shorter than the usual
|
|
504
|
+
// one: the task list that comes next still gets its own full try.
|
|
505
|
+
function lookUpLinkedWorktrees(ws, { timeoutMs = LINKED_WORKTREE_LOOKUP_MS } = {}) {
|
|
481
506
|
if (linkedWorktreeCache.has(ws)) return linkedWorktreeCache.get(ws);
|
|
482
|
-
let
|
|
507
|
+
let looked = { roots: [], failed: false };
|
|
483
508
|
try {
|
|
484
509
|
if (fs.existsSync(path.join(ws, '.git', 'worktrees'))) {
|
|
510
|
+
// A limit of 0 would mean no limit at all.
|
|
511
|
+
if (!(timeoutMs > 0)) throw new Error('no time left to ask git');
|
|
485
512
|
const out = require('child_process').execFileSync('git', ['worktree', 'list', '--porcelain'],
|
|
486
|
-
{ cwd: ws, encoding: 'utf8', timeout:
|
|
487
|
-
|
|
488
|
-
.
|
|
489
|
-
|
|
513
|
+
{ cwd: ws, encoding: 'utf8', timeout: timeoutMs, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
514
|
+
looked = {
|
|
515
|
+
roots: out.split('\n').filter(l => l.startsWith('worktree '))
|
|
516
|
+
.map(l => { const p = l.slice(9); try { return fs.realpathSync(p); } catch { return p; } })
|
|
517
|
+
.filter(p => p !== ws),
|
|
518
|
+
failed: false,
|
|
519
|
+
};
|
|
490
520
|
}
|
|
491
|
-
} catch {
|
|
492
|
-
|
|
493
|
-
|
|
521
|
+
} catch {
|
|
522
|
+
looked = { roots: [], failed: true };
|
|
523
|
+
if (timeoutMs < LINKED_WORKTREE_LOOKUP_MS) return looked;
|
|
524
|
+
}
|
|
525
|
+
linkedWorktreeCache.set(ws, looked);
|
|
526
|
+
return looked;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function linkedWorktreeRoots(ws) {
|
|
530
|
+
return lookUpLinkedWorktrees(ws).roots;
|
|
494
531
|
}
|
|
495
532
|
|
|
496
533
|
// The workspaces a workspace's task listing covers: the workspace itself and, for a main
|
|
@@ -3106,6 +3143,13 @@ function safeJSON(s) {
|
|
|
3106
3143
|
try { return JSON.parse(s); } catch { return null; }
|
|
3107
3144
|
}
|
|
3108
3145
|
|
|
3146
|
+
// Whether an error is the store being busy: another process holds the write lock.
|
|
3147
|
+
function isBusyError(e) {
|
|
3148
|
+
const msg = String(e && e.message || '');
|
|
3149
|
+
const code = e && (e.code || e.errcode);
|
|
3150
|
+
return /SQLITE_BUSY|database is locked/i.test(msg) || code === 'SQLITE_BUSY' || code === 5;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3109
3153
|
// Wrap a write op so SQLITE_BUSY (concurrent writers from other processes)
|
|
3110
3154
|
// retries with exponential backoff. busy_timeout pragma alone leaks busy
|
|
3111
3155
|
// errors under spawn-storm contention with node:sqlite (~3% raw lock rate
|
|
@@ -3118,10 +3162,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
3118
3162
|
try { return fn(); }
|
|
3119
3163
|
catch (e) {
|
|
3120
3164
|
lastErr = e;
|
|
3121
|
-
|
|
3122
|
-
const code = e && (e.code || e.errcode);
|
|
3123
|
-
const busy = /SQLITE_BUSY|database is locked/i.test(msg) || code === 'SQLITE_BUSY' || code === 5;
|
|
3124
|
-
if (!busy) throw e;
|
|
3165
|
+
if (!isBusyError(e)) throw e;
|
|
3125
3166
|
// Sleep synchronously, node:sqlite is sync; matches the rest of the API
|
|
3126
3167
|
sleepSync(delay + Math.floor(Math.random() * delay));
|
|
3127
3168
|
delay = Math.min(delay * 2, 500);
|
|
@@ -3133,6 +3174,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
3133
3174
|
module.exports = {
|
|
3134
3175
|
open,
|
|
3135
3176
|
withBusyRetry,
|
|
3177
|
+
isBusyError,
|
|
3136
3178
|
close,
|
|
3137
3179
|
getDbPath,
|
|
3138
3180
|
workspaceRoot,
|
|
@@ -3144,6 +3186,7 @@ module.exports = {
|
|
|
3144
3186
|
listTasks,
|
|
3145
3187
|
listTasksCached,
|
|
3146
3188
|
rowInWorkspace,
|
|
3189
|
+
lookUpLinkedWorktrees,
|
|
3147
3190
|
claimTask,
|
|
3148
3191
|
setLease,
|
|
3149
3192
|
renewLease,
|
package/lib/task-decision.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// marker is needs-human (CLI-879); decision is the same hold under a clearer
|
|
6
6
|
// name. Autonomous pickers must refuse these; list renderers must show them.
|
|
7
7
|
|
|
8
|
+
const { decisionChoiceText } = require('./task-explanation');
|
|
9
|
+
|
|
8
10
|
const DECISION_HOLD_TAGS = new Set([
|
|
9
11
|
'needs-human',
|
|
10
12
|
'needshuman',
|
|
@@ -41,10 +43,74 @@ function decisionMarkerFor(task) {
|
|
|
41
43
|
return isDecisionTask(task) ? DECISION_MARKER : '';
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function decisionRefFor(task) {
|
|
47
|
+
if (!task || typeof task !== 'object') return 'TASK';
|
|
48
|
+
const ref = task.display_id || task.legacy_ref || task.id || '';
|
|
49
|
+
const text = String(ref || '').trim();
|
|
50
|
+
if (!text) return 'TASK';
|
|
51
|
+
if (task.display_id || task.legacy_ref) return text;
|
|
52
|
+
return text.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8) || 'TASK';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function decisionShowCommand(task) {
|
|
56
|
+
return `atris task show ${decisionRefFor(task)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summarizeDecisionTask(task) {
|
|
60
|
+
const ref = decisionRefFor(task);
|
|
61
|
+
const title = String((task && task.title) || '').replace(/\s+/g, ' ').trim();
|
|
62
|
+
const choice = decisionChoiceText(task);
|
|
63
|
+
const command = `atris task show ${ref}`;
|
|
64
|
+
return {
|
|
65
|
+
id: (task && task.id) || null,
|
|
66
|
+
task_id: (task && task.id) || null,
|
|
67
|
+
ref,
|
|
68
|
+
display_id: (task && task.display_id) || ref,
|
|
69
|
+
title,
|
|
70
|
+
choice,
|
|
71
|
+
command,
|
|
72
|
+
show_command: command,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function summarizeSkippedDecisions(tasks = []) {
|
|
77
|
+
const list = (Array.isArray(tasks) ? tasks : [])
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map(summarizeDecisionTask);
|
|
80
|
+
const first = list[0] || null;
|
|
81
|
+
return {
|
|
82
|
+
reason: DECISION_REFUSE_REASON,
|
|
83
|
+
skipped_decision_count: list.length,
|
|
84
|
+
skipped_decision_refs: list.map((item) => item.ref),
|
|
85
|
+
skipped_decisions: list,
|
|
86
|
+
decisions: list,
|
|
87
|
+
choice: first ? first.choice : null,
|
|
88
|
+
command: first ? first.command : null,
|
|
89
|
+
show_command: first ? first.command : null,
|
|
90
|
+
next_command: first ? first.command : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatDecisionRefusalLines(tasks = []) {
|
|
95
|
+
const summary = summarizeSkippedDecisions(tasks);
|
|
96
|
+
const lines = [DECISION_REFUSE_REASON];
|
|
97
|
+
for (const item of summary.skipped_decisions) {
|
|
98
|
+
lines.push(` ${item.ref}: ${item.title}`);
|
|
99
|
+
lines.push(` choice: ${item.choice}`);
|
|
100
|
+
lines.push(` show: ${item.command}`);
|
|
101
|
+
}
|
|
102
|
+
if (summary.command) {
|
|
103
|
+
lines.push(`next: ${summary.command}`);
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
44
108
|
module.exports = {
|
|
45
109
|
DECISION_REFUSE_REASON,
|
|
46
110
|
taskTagTokens,
|
|
47
111
|
isDecisionHoldTag,
|
|
48
112
|
isDecisionTask,
|
|
49
113
|
decisionMarkerFor,
|
|
114
|
+
summarizeSkippedDecisions,
|
|
115
|
+
formatDecisionRefusalLines,
|
|
50
116
|
};
|
package/lib/task-explanation.js
CHANGED
|
@@ -213,6 +213,25 @@ function taskApprovalControls({
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
function decisionChoiceText(task) {
|
|
217
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
218
|
+
const explicitChoice = cleanText(
|
|
219
|
+
(task && task.choice)
|
|
220
|
+
|| metadata.choice
|
|
221
|
+
|| metadata.decision
|
|
222
|
+
|| metadata.question
|
|
223
|
+
);
|
|
224
|
+
if (explicitChoice) return endSentence(plainText(explicitChoice));
|
|
225
|
+
const explicit = explicitExplanationFields(metadata);
|
|
226
|
+
if (explicit.what_changes) return endSentence(plainText(explicit.what_changes));
|
|
227
|
+
if (explicit.why_it_matters) return endSentence(plainText(explicit.why_it_matters));
|
|
228
|
+
const goal = cleanText(explanationGoalText(task));
|
|
229
|
+
if (goal) return endSentence(plainText(goal));
|
|
230
|
+
const title = cleanText(task && task.title);
|
|
231
|
+
if (title) return endSentence(plainText(title));
|
|
232
|
+
return 'Decide whether to approve this work, revise it, or remove the decision hold.';
|
|
233
|
+
}
|
|
234
|
+
|
|
216
235
|
function approvalLines(approval, { indent = '' } = {}) {
|
|
217
236
|
const lines = [`${indent}${approval.question}`];
|
|
218
237
|
if (approval.approve.enabled) lines.push(`${indent}Approve: ${approval.approve.command}`);
|
|
@@ -228,6 +247,7 @@ module.exports = {
|
|
|
228
247
|
explicitDoneLooksLike,
|
|
229
248
|
plainText,
|
|
230
249
|
taskExplanation,
|
|
250
|
+
decisionChoiceText,
|
|
231
251
|
explanationFieldsFromInput,
|
|
232
252
|
explanationLines,
|
|
233
253
|
explanationMarkdownLines,
|
package/lib/yes-lock.js
CHANGED
|
@@ -593,7 +593,7 @@ const ATRIS_INTERNAL = new Set([
|
|
|
593
593
|
'version', 'help', 'next', 'atris', 'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'learn', 'lesson', 'taste', 'teach',
|
|
594
594
|
'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'watch', 'live', 'align', 'terminal', 'computer', 'diff', 'youtube', 'x-search',
|
|
595
595
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'campaign', 'agents', 'probe', 'worktree', 'caretaker', 'aeo', 'slop', 'voice', 'strings', 'write',
|
|
596
|
-
'security-review', 'secure', 'theme', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'recap', 'report', 'clarity', 'interview', 'moves', 'unknowns', 'avail',
|
|
596
|
+
'security-review', 'secure', 'sentinel', 'theme', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'recap', 'report', 'clarity', 'interview', 'moves', 'unknowns', 'avail',
|
|
597
597
|
'scoreboard', 'sync-checkout', 'zen', 'ci', 'balance', 'usage', 'agent-client', 'transactions', 'hires', 'design', 'mcp', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
598
598
|
'browse', 'sleep', 'wake', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'engine',
|
|
599
599
|
'engines', 'link', 'playbook', 'brief', 'inbox', 'friends', 'rsi',
|
package/package.json
CHANGED