atris 3.15.56 → 3.16.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 +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +11 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +32 -31
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +197 -22
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/computer.js +45 -83
- package/commands/improve.js +501 -0
- package/commands/integrations.js +228 -0
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/radar.js +181 -56
- package/commands/skill.js +37 -6
- package/commands/soul.js +0 -4
- package/commands/task.js +5582 -517
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +41 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +34 -0
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- package/lib/section-merge.js +0 -196
package/commands/now.js
CHANGED
|
@@ -2,7 +2,10 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const NOW_PATH = path.join('atris', 'now.md');
|
|
5
|
+
const TASK_EPISODES_PATH = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
6
|
+
const CAREER_XP_RECEIPTS_PATH = path.join('.atris', 'state', 'career_xp_receipts.jsonl');
|
|
5
7
|
const EXECUTABLE_TASK_STATUSES = new Set(['open', 'claimed']);
|
|
8
|
+
const TASK_RECEIPT_EVENTS = new Set(['proof_ready', 'reviewed', 'completed']);
|
|
6
9
|
|
|
7
10
|
function formatLocalDate(date = new Date()) {
|
|
8
11
|
const year = String(date.getFullYear());
|
|
@@ -115,6 +118,87 @@ function countJournalCompletedReceipts(filePath) {
|
|
|
115
118
|
return countMatches(filePath, /^-\s+\*\*C\d+:/gm);
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function readJsonlRows(filePath) {
|
|
122
|
+
if (!fs.existsSync(filePath)) return [];
|
|
123
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
124
|
+
.split(/\r?\n/)
|
|
125
|
+
.map(line => line.trim())
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map((line) => {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(line);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function localDateKey(value) {
|
|
138
|
+
if (!value) return null;
|
|
139
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
140
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
141
|
+
return formatLocalDate(date);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeRoot(value) {
|
|
145
|
+
if (!value) return null;
|
|
146
|
+
try {
|
|
147
|
+
return fs.realpathSync(value);
|
|
148
|
+
} catch {
|
|
149
|
+
return path.resolve(String(value));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function rowMatchesWorkspace(rowRoot, root) {
|
|
154
|
+
if (!rowRoot) return true;
|
|
155
|
+
return normalizeRoot(rowRoot) === normalizeRoot(root);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function taskReceiptProof(row) {
|
|
159
|
+
return String(
|
|
160
|
+
row?.proof
|
|
161
|
+
|| row?.proof_ref
|
|
162
|
+
|| row?.review?.proof
|
|
163
|
+
|| row?.state?.metadata?.latest_agent_proof
|
|
164
|
+
|| '',
|
|
165
|
+
).trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function taskReceiptKey(row, fallback) {
|
|
169
|
+
const episodeId = row?.episode_id || row?.source_episode_id;
|
|
170
|
+
if (episodeId) return `episode:${episodeId}`;
|
|
171
|
+
if (row?.receipt_id) return `receipt:${row.receipt_id}`;
|
|
172
|
+
if (row?.task_id || row?.source_task_id) return `task:${row.task_id || row.source_task_id}:${fallback}`;
|
|
173
|
+
return `row:${fallback}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function countTaskReceiptsToday(root = process.cwd(), date = new Date()) {
|
|
177
|
+
const targetDay = formatLocalDate(date);
|
|
178
|
+
const stateDir = path.join(root, '.atris', 'state');
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
|
|
181
|
+
for (const row of readJsonlRows(path.join(stateDir, 'task_episodes.jsonl'))) {
|
|
182
|
+
if (localDateKey(row?.created_at) !== targetDay) continue;
|
|
183
|
+
if (!rowMatchesWorkspace(row?.workspace_root, root)) continue;
|
|
184
|
+
if (!taskReceiptProof(row)) continue;
|
|
185
|
+
const eventType = String(row?.action?.event_type || '').toLowerCase();
|
|
186
|
+
if (eventType && !TASK_RECEIPT_EVENTS.has(eventType)) continue;
|
|
187
|
+
seen.add(taskReceiptKey(row, seen.size));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const row of readJsonlRows(path.join(stateDir, 'career_xp_receipts.jsonl'))) {
|
|
191
|
+
if (localDateKey(row?.accepted_at || row?.created_at || row?.ts) !== targetDay) continue;
|
|
192
|
+
if (!rowMatchesWorkspace(row?.workspace_root, root)) continue;
|
|
193
|
+
if (!taskReceiptProof(row)) continue;
|
|
194
|
+
const source = String(row?.source_type || row?.receipt_id || row?.source || '').toLowerCase();
|
|
195
|
+
if (!source.includes('task')) continue;
|
|
196
|
+
seen.add(taskReceiptKey(row, seen.size));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return seen.size;
|
|
200
|
+
}
|
|
201
|
+
|
|
118
202
|
function currentJournalPath(root = process.cwd()) {
|
|
119
203
|
const now = new Date();
|
|
120
204
|
const year = String(now.getFullYear());
|
|
@@ -129,7 +213,8 @@ function renderDefaultNow(root = process.cwd()) {
|
|
|
129
213
|
const journalPath = currentJournalPath(root);
|
|
130
214
|
const openTodoCount = countOpenWorkItems(root, todoPath);
|
|
131
215
|
const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
|
|
132
|
-
const
|
|
216
|
+
const taskReceiptCount = countTaskReceiptsToday(root);
|
|
217
|
+
const completedCount = taskReceiptCount || countJournalCompletedReceipts(journalPath);
|
|
133
218
|
const generated = todayIso();
|
|
134
219
|
|
|
135
220
|
return `# now
|
|
@@ -330,11 +415,14 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
|
|
|
330
415
|
|
|
331
416
|
module.exports = {
|
|
332
417
|
NOW_PATH,
|
|
418
|
+
TASK_EPISODES_PATH,
|
|
419
|
+
CAREER_XP_RECEIPTS_PATH,
|
|
333
420
|
ensureNowFile,
|
|
334
421
|
formatLocalDate,
|
|
335
422
|
countJournalCompletedReceipts,
|
|
336
423
|
countOpenWorkItems,
|
|
337
424
|
countOpenTodoItems,
|
|
425
|
+
countTaskReceiptsToday,
|
|
338
426
|
findChildWorkspaces,
|
|
339
427
|
isGeneratedNowFile,
|
|
340
428
|
nowAtris,
|
package/commands/radar.js
CHANGED
|
@@ -114,26 +114,19 @@ function loadTasks(root, deps) {
|
|
|
114
114
|
if (!deps.exists(file)) return [];
|
|
115
115
|
const payload = safeJson(deps.readFile(file, 'utf8'), {});
|
|
116
116
|
const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
|
|
117
|
-
return tasks.map(task => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
Object.defineProperty(row, 'routing_text', {
|
|
132
|
-
enumerable: false,
|
|
133
|
-
value: messages.map(message => message && message.content).filter(Boolean).join(' '),
|
|
134
|
-
});
|
|
135
|
-
return row;
|
|
136
|
-
});
|
|
117
|
+
return tasks.map(task => ({
|
|
118
|
+
id: task.id,
|
|
119
|
+
display_id: task.display_id,
|
|
120
|
+
legacy_ref: task.legacy_ref,
|
|
121
|
+
title: task.title,
|
|
122
|
+
status: task.status,
|
|
123
|
+
tag: task.tag,
|
|
124
|
+
workspace_root: task.workspace_root,
|
|
125
|
+
claimed_by: task.claimed_by,
|
|
126
|
+
assigned_to: task.metadata?.assigned_to || task.assigned_to || null,
|
|
127
|
+
metadata: task.metadata || {},
|
|
128
|
+
messages: Array.isArray(task.messages) ? task.messages : [],
|
|
129
|
+
}));
|
|
137
130
|
}
|
|
138
131
|
|
|
139
132
|
function findTaskWorkspaceRoot(cwd, deps) {
|
|
@@ -172,7 +165,7 @@ function untaskedAction(agent, taskWorkspaceRoot, tasks) {
|
|
|
172
165
|
if (reason === 'cwd unknown') return `inspect pid ${pid} cwd with lsof`;
|
|
173
166
|
if (reason === 'no active task') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task next --as ${actor}`;
|
|
174
167
|
if (reason === 'empty task projection') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task new "<small concrete title>" --tag ops`;
|
|
175
|
-
return `inspect ${agent.cwd || 'unknown cwd'} for missing Atris task plane or close pid ${pid} if idle`;
|
|
168
|
+
return `inspect ${agent.cwd || 'unknown cwd'} for missing Atris task plane or close pid ${pid} only with operator approval if idle`;
|
|
176
169
|
}
|
|
177
170
|
|
|
178
171
|
function readJsonFile(file, deps, fallback = null) {
|
|
@@ -340,8 +333,6 @@ function loadBusinessCollaboration(root, deps, team = {}) {
|
|
|
340
333
|
const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
|
|
341
334
|
const scorecards = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps);
|
|
342
335
|
const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
|
|
343
|
-
const runtimeComputer = runtime && (runtime.workspace_id || runtime.business_id || runtime.scope === 'local-business-computer') ? 1 : 0;
|
|
344
|
-
const computers = Math.max(computerDirs, runtimeComputer);
|
|
345
336
|
const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
|
|
346
337
|
const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
|
|
347
338
|
const hasTeam = Number(team.total || 0) > 0;
|
|
@@ -370,7 +361,7 @@ function loadBusinessCollaboration(root, deps, team = {}) {
|
|
|
370
361
|
} : null,
|
|
371
362
|
onboarding: { packs: ingestPacks, starter_briefs: starterBriefs, first_loops: firstLoops, one_pagers: onePagers, reports },
|
|
372
363
|
proof: { events, episodes, scorecards, receipts: localReceipts },
|
|
373
|
-
computers,
|
|
364
|
+
computers: computerDirs,
|
|
374
365
|
team_members: Number(team.total || 0),
|
|
375
366
|
active_goal_members: Number(team.active_goal_members || 0),
|
|
376
367
|
share_ready: missing.length === 0,
|
|
@@ -418,15 +409,47 @@ function taskRef(task) {
|
|
|
418
409
|
return task ? (task.display_id || task.legacy_ref || task.id || '-') : '-';
|
|
419
410
|
}
|
|
420
411
|
|
|
421
|
-
function
|
|
412
|
+
function taskOwnerMatchesAgent(task, agent) {
|
|
413
|
+
const actor = String(agent?.agent || '').toLowerCase();
|
|
414
|
+
if (!actor) return false;
|
|
415
|
+
return [
|
|
416
|
+
task.assigned_to,
|
|
417
|
+
task.claimed_by,
|
|
418
|
+
task.metadata?.assigned_to,
|
|
419
|
+
task.metadata?.claimed_by,
|
|
420
|
+
task.metadata?.owner,
|
|
421
|
+
]
|
|
422
|
+
.filter(Boolean)
|
|
423
|
+
.some(owner => String(owner).toLowerCase() === actor);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function taskForCwd(tasks, cwd, workspaceRoot = cwd, agent = null) {
|
|
422
427
|
if (!cwd && !workspaceRoot) return null;
|
|
423
428
|
const matchesWorkspace = task => !task.workspace_root || task.workspace_root === cwd || task.workspace_root === workspaceRoot;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
429
|
+
const candidates = tasks.filter(matchesWorkspace);
|
|
430
|
+
const firstByStatus = status => {
|
|
431
|
+
const statusMatches = candidates.filter(task => task.status === status);
|
|
432
|
+
return statusMatches.find(task => taskOwnerMatchesAgent(task, agent)) || statusMatches[0] || null;
|
|
433
|
+
};
|
|
434
|
+
return firstByStatus('claimed')
|
|
435
|
+
|| firstByStatus('open')
|
|
436
|
+
|| firstByStatus('review')
|
|
427
437
|
|| null;
|
|
428
438
|
}
|
|
429
439
|
|
|
440
|
+
function taskBindingForProjection(task) {
|
|
441
|
+
if (!task) return { task_source: null, task_scope: null };
|
|
442
|
+
return {
|
|
443
|
+
task_source: 'repo_task_projection',
|
|
444
|
+
task_scope: 'repo',
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function taskSourceLabel(sources = []) {
|
|
449
|
+
if (sources.includes('repo_task_projection')) return 'repo projection; verify ownership';
|
|
450
|
+
return sources.filter(Boolean).join(', ');
|
|
451
|
+
}
|
|
452
|
+
|
|
430
453
|
function ownerForTask(task) {
|
|
431
454
|
if (!task) return '-';
|
|
432
455
|
return task.assigned_to || task.claimed_by || task.metadata?.assigned_to || '-';
|
|
@@ -449,33 +472,21 @@ function taskSessionAction(agent, task, taskWorkspaceRoot) {
|
|
|
449
472
|
: 'atris task reviews --limit 5';
|
|
450
473
|
if (task.status === 'review') {
|
|
451
474
|
if (task.metadata?.agent_certified) {
|
|
452
|
-
return `handoff complete for ${ref};
|
|
475
|
+
return `handoff complete for ${ref}; claim fresh work as ${actor} or close pid ${pid} only with operator approval`;
|
|
453
476
|
}
|
|
454
477
|
return `review or hand off ${ref}: ${reviewCommand}`;
|
|
455
478
|
}
|
|
456
|
-
if (ownerActionRequired(task)) return `owner-gated ${ref}; close pid ${pid}
|
|
479
|
+
if (ownerActionRequired(task)) return `owner-gated ${ref}; wait for owner action, do not start duplicate work; close pid ${pid} only with operator approval`;
|
|
457
480
|
return null;
|
|
458
481
|
}
|
|
459
482
|
|
|
460
|
-
function summarize(tasks, missions, worktrees, agents) {
|
|
461
|
-
const count = (rows, pred) => rows.filter(pred).length;
|
|
462
|
-
return {
|
|
463
|
-
agents: { total: agents.length, active: count(agents, a => a.status === 'active'), stopped: count(agents, a => a.status !== 'active') },
|
|
464
|
-
tasks: {
|
|
465
|
-
open: count(tasks, t => t.status === 'open'),
|
|
466
|
-
claimed: count(tasks, t => t.status === 'claimed'),
|
|
467
|
-
review: count(tasks, t => t.status === 'review'),
|
|
468
|
-
certifiedReview: count(tasks, t => t.status === 'review' && t.metadata && t.metadata.agent_certified),
|
|
469
|
-
},
|
|
470
|
-
missions: { running: count(missions, m => m.status === 'running'), stale: count(missions, m => m.stale) },
|
|
471
|
-
worktrees: { total: worktrees.length, dirty: count(worktrees, w => Number(w.dirty) > 0) },
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
483
|
function ownerActionRequired(task) {
|
|
476
484
|
const metadata = task?.metadata || {};
|
|
477
485
|
if (metadata.owner_action_required === true || metadata.agent_executable === false) return true;
|
|
478
486
|
if (String(metadata.agent_executable || '').toLowerCase() === 'false') return true;
|
|
487
|
+
const recentMessages = Array.isArray(task?.messages)
|
|
488
|
+
? task.messages.slice(-8).map(message => message?.content || message?.payload?.content || '')
|
|
489
|
+
: [];
|
|
479
490
|
const text = [
|
|
480
491
|
task?.title,
|
|
481
492
|
task?.tag,
|
|
@@ -485,12 +496,32 @@ function ownerActionRequired(task) {
|
|
|
485
496
|
metadata.latest_agent_proof,
|
|
486
497
|
metadata.latest_agent_lesson,
|
|
487
498
|
task?.routing_text,
|
|
499
|
+
...recentMessages,
|
|
488
500
|
].filter(Boolean).join(' ').toLowerCase();
|
|
489
501
|
if (text.includes('owner_action_required') || text.includes('agent_executable=false')) return true;
|
|
490
502
|
if (text.includes('owner-only') || text.includes('owner only') || text.includes('not agent-executable')) return true;
|
|
503
|
+
const productionGateEligible = !['radar', 'task-plane', 'review'].includes(String(task?.tag || '').toLowerCase());
|
|
504
|
+
if (productionGateEligible && (text.includes('human/production gated') || text.includes('production execution proof is still missing'))) return true;
|
|
505
|
+
if (productionGateEligible && text.includes('remaining blocker') && text.includes('deploy receipt') && text.includes('live canary')) return true;
|
|
506
|
+
if (productionGateEligible && text.includes('human approval required') && (text.includes('production deploy') || text.includes('feedback mutation'))) return true;
|
|
491
507
|
return text.includes('owner') && text.includes('billing') && (text.includes('spending limit') || text.includes('failed payments'));
|
|
492
508
|
}
|
|
493
509
|
|
|
510
|
+
function summarize(tasks, missions, worktrees, agents) {
|
|
511
|
+
const count = (rows, pred) => rows.filter(pred).length;
|
|
512
|
+
return {
|
|
513
|
+
agents: { total: agents.length, active: count(agents, a => a.status === 'active'), stopped: count(agents, a => a.status !== 'active') },
|
|
514
|
+
tasks: {
|
|
515
|
+
open: count(tasks, t => t.status === 'open'),
|
|
516
|
+
claimed: count(tasks, t => t.status === 'claimed'),
|
|
517
|
+
review: count(tasks, t => t.status === 'review'),
|
|
518
|
+
certifiedReview: count(tasks, t => t.status === 'review' && t.metadata && t.metadata.agent_certified),
|
|
519
|
+
},
|
|
520
|
+
missions: { running: count(missions, m => m.status === 'running'), stale: count(missions, m => m.stale) },
|
|
521
|
+
worktrees: { total: worktrees.length, dirty: count(worktrees, w => Number(w.dirty) > 0) },
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
494
525
|
function nextAction(tasks, missions, worktrees, agents, os = {}) {
|
|
495
526
|
const activeTasks = tasks.filter(t => t.status === 'claimed' || t.status === 'open');
|
|
496
527
|
const activeTask = activeTasks.find(t => !ownerActionRequired(t));
|
|
@@ -529,14 +560,16 @@ function collectRadar(options = {}) {
|
|
|
529
560
|
const agents = collectAgents(deps).map(agent => {
|
|
530
561
|
const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
|
|
531
562
|
const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
|
|
532
|
-
const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot);
|
|
563
|
+
const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot, agent);
|
|
533
564
|
const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
|
|
565
|
+
const taskBinding = taskBindingForProjection(task);
|
|
534
566
|
return {
|
|
535
567
|
...agent,
|
|
536
568
|
task: taskRef(task),
|
|
537
569
|
task_status: task?.status || null,
|
|
538
570
|
owner: ownerForTask(task),
|
|
539
571
|
task_workspace: taskWorkspaceRoot ? repoLabel(taskWorkspaceRoot) : null,
|
|
572
|
+
...taskBinding,
|
|
540
573
|
task_reason: taskReason,
|
|
541
574
|
task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
|
|
542
575
|
};
|
|
@@ -646,19 +679,73 @@ function sortedAgents(agents = []) {
|
|
|
646
679
|
});
|
|
647
680
|
}
|
|
648
681
|
|
|
682
|
+
function executableAgentLanes(agents = []) {
|
|
683
|
+
return sortedAgents(agents).filter(agent => {
|
|
684
|
+
if (agent.status !== 'active') return false;
|
|
685
|
+
if (!agent.task || agent.task === '-') return false;
|
|
686
|
+
if (!['claimed', 'open'].includes(agent.task_status)) return false;
|
|
687
|
+
return !agent.task_reason;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function formatExecutableLane(agent) {
|
|
692
|
+
const repo = agent.repo || agent.cwd || 'unknown repo';
|
|
693
|
+
const actor = agent.agent || 'agent';
|
|
694
|
+
return `continue ${agent.task} in ${repo} as ${actor}`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function certifiedReviewLanes(agents = []) {
|
|
698
|
+
return sortedAgents(agents).filter(agent => {
|
|
699
|
+
if (agent.status !== 'active') return false;
|
|
700
|
+
if (!agent.task || agent.task === '-') return false;
|
|
701
|
+
if (agent.task_status !== 'review') return false;
|
|
702
|
+
return agent.task_reason === 'certified review';
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function formatReviewCheckpoint(agent) {
|
|
707
|
+
const repo = agent.repo || agent.cwd || 'unknown repo';
|
|
708
|
+
return `accept/revise ${agent.task} in ${repo}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
649
711
|
function agentProcessNextAction(agents = [], fallback = 'no obvious process action') {
|
|
650
712
|
const stopped = agents.filter(agent => agent.status !== 'active').length;
|
|
651
713
|
if (stopped > 0) return `inspect ${stopped} stopped agent session${stopped === 1 ? '' : 's'}`;
|
|
652
714
|
const ownerGated = agents.filter(agent => agent.task_reason === 'owner action required');
|
|
653
715
|
if (ownerGated.length > 0) {
|
|
654
716
|
const tasks = [...new Set(ownerGated.map(agent => agent.task).filter(Boolean))].slice(0, 3).join(', ');
|
|
655
|
-
|
|
717
|
+
const projectionBound = ownerGated.some(agent => agent.task_source === 'repo_task_projection');
|
|
718
|
+
const executable = executableAgentLanes(agents)[0];
|
|
719
|
+
const reviewCheckpoint = executable ? null : certifiedReviewLanes(agents)[0];
|
|
720
|
+
const ownerGateAction = executable
|
|
721
|
+
? `wait for owner action; executable lane: ${formatExecutableLane(executable)}; avoid duplicate work, close only with operator approval`
|
|
722
|
+
: reviewCheckpoint
|
|
723
|
+
? `wait for owner action; review checkpoint: ${formatReviewCheckpoint(reviewCheckpoint)}; avoid duplicate work, close only with operator approval`
|
|
724
|
+
: 'wait for owner action, avoid duplicate work, close only with operator approval';
|
|
725
|
+
if (projectionBound) {
|
|
726
|
+
return `owner-gated repo projection covers ${ownerGated.length} session${ownerGated.length === 1 ? '' : 's'}${tasks ? ` on ${tasks}` : ''}; verify ownership, ${ownerGateAction}`;
|
|
727
|
+
}
|
|
728
|
+
return `owner gate blocks ${ownerGated.length} session${ownerGated.length === 1 ? '' : 's'}${tasks ? ` on ${tasks}` : ''}; ${ownerGateAction}`;
|
|
656
729
|
}
|
|
657
730
|
const taskLoad = summarizeTaskLoad(agents);
|
|
731
|
+
const activePileup = taskLoad.find(row => row.sessions > 1 && !row.status.split(/,\s*/).includes('review'));
|
|
732
|
+
if (activePileup) {
|
|
733
|
+
const source = taskSourceLabel(activePileup.task_sources);
|
|
734
|
+
const sourceHint = source ? `; ${source}` : '';
|
|
735
|
+
return `inspect ${activePileup.sessions} sessions on ${activePileup.task} (${activePileup.cpu.toFixed(1)}% CPU${sourceHint})`;
|
|
736
|
+
}
|
|
658
737
|
const reviewBound = taskLoad.find(row => row.status.split(/,\s*/).includes('review'));
|
|
659
|
-
if (reviewBound)
|
|
738
|
+
if (reviewBound) {
|
|
739
|
+
const source = taskSourceLabel(reviewBound.task_sources);
|
|
740
|
+
const sourceHint = source ? `; ${source}` : '';
|
|
741
|
+
return `close or hand off ${reviewBound.sessions} session${reviewBound.sessions === 1 ? '' : 's'} still bound to review task ${reviewBound.task}${sourceHint}`;
|
|
742
|
+
}
|
|
660
743
|
const pileup = taskLoad.find(row => row.sessions > 1);
|
|
661
|
-
if (pileup)
|
|
744
|
+
if (pileup) {
|
|
745
|
+
const source = taskSourceLabel(pileup.task_sources);
|
|
746
|
+
const sourceHint = source ? `; ${source}` : '';
|
|
747
|
+
return `inspect ${pileup.sessions} sessions on ${pileup.task} (${pileup.cpu.toFixed(1)}% CPU${sourceHint})`;
|
|
748
|
+
}
|
|
662
749
|
const untasked = agents.filter(agent => !agent.task || agent.task === '-').length;
|
|
663
750
|
if (untasked > 0) {
|
|
664
751
|
const reasons = summarizeUntaskedReasons(agents);
|
|
@@ -696,6 +783,8 @@ function summarizeTaskLoad(agents = []) {
|
|
|
696
783
|
statuses: new Set(),
|
|
697
784
|
owners: new Set(),
|
|
698
785
|
repos: new Set(),
|
|
786
|
+
task_sources: new Set(),
|
|
787
|
+
task_scopes: new Set(),
|
|
699
788
|
pids: [],
|
|
700
789
|
});
|
|
701
790
|
}
|
|
@@ -707,6 +796,8 @@ function summarizeTaskLoad(agents = []) {
|
|
|
707
796
|
if (agent.task_status) row.statuses.add(agent.task_status);
|
|
708
797
|
if (agent.owner && agent.owner !== '-') row.owners.add(agent.owner);
|
|
709
798
|
if (agent.repo) row.repos.add(agent.repo);
|
|
799
|
+
if (agent.task_source) row.task_sources.add(agent.task_source);
|
|
800
|
+
if (agent.task_scope) row.task_scopes.add(agent.task_scope);
|
|
710
801
|
if (agent.pid) row.pids.push(agent.pid);
|
|
711
802
|
}
|
|
712
803
|
return [...byTask.values()]
|
|
@@ -721,6 +812,8 @@ function summarizeTaskLoad(agents = []) {
|
|
|
721
812
|
status: statuses.join(', ') || '-',
|
|
722
813
|
owners: [...row.owners].sort(),
|
|
723
814
|
repos: [...row.repos].sort(),
|
|
815
|
+
task_sources: [...row.task_sources].sort(),
|
|
816
|
+
task_scopes: [...row.task_scopes].sort(),
|
|
724
817
|
pids: row.pids.sort((a, b) => number(a) - number(b)),
|
|
725
818
|
attention: row.sessions > 1 || statuses.includes('review'),
|
|
726
819
|
};
|
|
@@ -753,6 +846,26 @@ function agentTopPayload(data) {
|
|
|
753
846
|
};
|
|
754
847
|
}
|
|
755
848
|
|
|
849
|
+
function representativeAgentsByTask(agents = [], limit = 8) {
|
|
850
|
+
const selected = [];
|
|
851
|
+
const selectedRows = new Set();
|
|
852
|
+
const seenTasks = new Set();
|
|
853
|
+
for (const agent of agents) {
|
|
854
|
+
const task = agent.task || '-';
|
|
855
|
+
if (seenTasks.has(task)) continue;
|
|
856
|
+
seenTasks.add(task);
|
|
857
|
+
selected.push(agent);
|
|
858
|
+
selectedRows.add(agent);
|
|
859
|
+
if (selected.length >= limit) return selected;
|
|
860
|
+
}
|
|
861
|
+
for (const agent of agents) {
|
|
862
|
+
if (selected.length >= limit) break;
|
|
863
|
+
if (selectedRows.has(agent)) continue;
|
|
864
|
+
selected.push(agent);
|
|
865
|
+
}
|
|
866
|
+
return selected;
|
|
867
|
+
}
|
|
868
|
+
|
|
756
869
|
function renderAgentTop(data) {
|
|
757
870
|
const payload = agentTopPayload(data);
|
|
758
871
|
const lines = [];
|
|
@@ -760,6 +873,9 @@ function renderAgentTop(data) {
|
|
|
760
873
|
lines.push('');
|
|
761
874
|
lines.push(`Agents: ${payload.summary.active}/${payload.summary.total} active; ${payload.summary.untasked} untasked; CPU ${payload.summary.cpu.toFixed(1)}%; MEM ${payload.summary.mem.toFixed(1)}%`);
|
|
762
875
|
lines.push(`Next: ${payload.next_action}`);
|
|
876
|
+
if (payload.agents.some(agent => agent.task_source === 'repo_task_projection')) {
|
|
877
|
+
lines.push('Task binding: repo projection; verify ownership before assuming every session owns the displayed task.');
|
|
878
|
+
}
|
|
763
879
|
lines.push('');
|
|
764
880
|
lines.push(`${truncate('PID', 7)} ${truncate('AGENT', 8)} ${truncate('CPU', 6)} ${truncate('MEM', 6)} ${truncate('REPO', 24)} ${truncate('BRANCH', 16)} ${truncate('TASK', 10)} ${truncate('STATE', 8)}`);
|
|
765
881
|
for (const agent of payload.agents.slice(0, 32)) {
|
|
@@ -774,19 +890,26 @@ function renderAgentTop(data) {
|
|
|
774
890
|
lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'}: ${agent.task_reason || 'unmapped'} -> ${agent.task_action || 'inspect session'}`);
|
|
775
891
|
}
|
|
776
892
|
}
|
|
777
|
-
const
|
|
893
|
+
const ownerGatedAll = payload.agents.filter(row => row.task_reason === 'owner action required');
|
|
894
|
+
const ownerGatedAgents = representativeAgentsByTask(ownerGatedAll, 8);
|
|
778
895
|
if (ownerGatedAgents.length) {
|
|
779
896
|
lines.push('');
|
|
780
|
-
|
|
897
|
+
const shown = ownerGatedAll.length > ownerGatedAgents.length ? `; showing ${ownerGatedAgents.length}` : '';
|
|
898
|
+
const projectionBound = ownerGatedAll.some(agent => agent.task_source === 'repo_task_projection');
|
|
899
|
+
const label = projectionBound ? 'Owner-gated projection' : 'Owner-gated';
|
|
900
|
+
const actionText = projectionBound ? 'verify ownership and wait on human/owner action' : 'waiting on human/owner action';
|
|
901
|
+
lines.push(`${label}: ${ownerGatedAll.length} session${ownerGatedAll.length === 1 ? '' : 's'} ${actionText}${shown}; do not start duplicate work.`);
|
|
781
902
|
for (const agent of ownerGatedAgents) {
|
|
782
903
|
lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'} ${agent.task}: ${agent.task_action || 'wait for owner action'}`);
|
|
783
904
|
}
|
|
784
905
|
}
|
|
785
|
-
const
|
|
786
|
-
|
|
906
|
+
const reviewAll = payload.agents.filter(row => row.task_reason === 'certified review' || row.task_reason === 'review task');
|
|
907
|
+
const reviewAgents = reviewAll.slice(0, 8);
|
|
908
|
+
if (reviewAgents.length) {
|
|
787
909
|
lines.push('');
|
|
788
|
-
|
|
789
|
-
|
|
910
|
+
const shown = reviewAll.length > reviewAgents.length ? `; showing ${reviewAgents.length}` : '';
|
|
911
|
+
lines.push(`Review-bound: ${reviewAll.length} session${reviewAll.length === 1 ? '' : 's'} should hand off or claim fresh work${shown}.`);
|
|
912
|
+
for (const agent of reviewAgents) {
|
|
790
913
|
lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'} ${agent.task}: ${agent.task_reason || 'review'} -> ${agent.task_action || 'close or hand off session'}`);
|
|
791
914
|
}
|
|
792
915
|
}
|
|
@@ -796,7 +919,9 @@ function renderAgentTop(data) {
|
|
|
796
919
|
lines.push(`Task load: ${payload.summary.task_pileups} pileup${payload.summary.task_pileups === 1 ? '' : 's'}, ${payload.summary.review_bound_tasks} review-bound task${payload.summary.review_bound_tasks === 1 ? '' : 's'}.`);
|
|
797
920
|
for (const row of taskLoadRows) {
|
|
798
921
|
const repoText = row.repos.slice(0, 3).join(', ') || '-';
|
|
799
|
-
|
|
922
|
+
const source = taskSourceLabel(row.task_sources);
|
|
923
|
+
const sourceText = source ? `, ${source}` : '';
|
|
924
|
+
lines.push(`- ${row.task}: ${row.sessions} sessions, ${row.cpu.toFixed(1)}% CPU, ${row.status}, ${repoText}${sourceText}`);
|
|
800
925
|
}
|
|
801
926
|
}
|
|
802
927
|
return lines.join('\n');
|
package/commands/skill.js
CHANGED
|
@@ -88,6 +88,39 @@ function findAllSkills(skillsDir) {
|
|
|
88
88
|
return skills;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function localSkillsDir() {
|
|
92
|
+
return path.join(process.cwd(), 'atris', 'skills');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function bundledSkillsDir() {
|
|
96
|
+
return path.join(__dirname, '..', 'atris', 'skills');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readableSkillRoots() {
|
|
100
|
+
const roots = [localSkillsDir(), bundledSkillsDir()];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
return roots.filter((root) => {
|
|
103
|
+
if (!root || !fs.existsSync(root)) return false;
|
|
104
|
+
const real = fs.realpathSync(root);
|
|
105
|
+
if (seen.has(real)) return false;
|
|
106
|
+
seen.add(real);
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function findReadableSkills() {
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const skills = [];
|
|
114
|
+
for (const root of readableSkillRoots()) {
|
|
115
|
+
for (const skill of findAllSkills(root)) {
|
|
116
|
+
if (seen.has(skill.folder)) continue;
|
|
117
|
+
seen.add(skill.folder);
|
|
118
|
+
skills.push(skill);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return skills;
|
|
122
|
+
}
|
|
123
|
+
|
|
91
124
|
// --- Audit Checks ---
|
|
92
125
|
|
|
93
126
|
function runAuditChecks(skill) {
|
|
@@ -355,11 +388,10 @@ function generateTags(folderName, description) {
|
|
|
355
388
|
// --- Subcommand Handlers ---
|
|
356
389
|
|
|
357
390
|
function skillList() {
|
|
358
|
-
const
|
|
359
|
-
const skills = findAllSkills(skillsDir);
|
|
391
|
+
const skills = findReadableSkills();
|
|
360
392
|
|
|
361
393
|
if (skills.length === 0) {
|
|
362
|
-
console.log('No skills found in
|
|
394
|
+
console.log('No skills found in local or bundled Atris skill roots.');
|
|
363
395
|
return;
|
|
364
396
|
}
|
|
365
397
|
|
|
@@ -396,15 +428,14 @@ function skillList() {
|
|
|
396
428
|
}
|
|
397
429
|
|
|
398
430
|
function skillAudit(name) {
|
|
399
|
-
const
|
|
400
|
-
const allSkills = findAllSkills(skillsDir);
|
|
431
|
+
const allSkills = findReadableSkills();
|
|
401
432
|
|
|
402
433
|
const targets = name === '--all'
|
|
403
434
|
? allSkills
|
|
404
435
|
: allSkills.filter(s => s.folder === name || s.leafFolder === name);
|
|
405
436
|
|
|
406
437
|
if (targets.length === 0) {
|
|
407
|
-
console.error(`Skill "${name}" not found. Run "atris skill list" to see available skills.`);
|
|
438
|
+
console.error(`Skill "${name}" not found. Run "atris skill list" to see available local and bundled skills.`);
|
|
408
439
|
process.exit(1);
|
|
409
440
|
}
|
|
410
441
|
|
package/commands/soul.js
CHANGED
|
@@ -26,10 +26,6 @@ function readFile(filePath) {
|
|
|
26
26
|
try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function readJson(filePath) {
|
|
30
|
-
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
function countFiles(dir) {
|
|
34
30
|
try { return fs.readdirSync(dir, { recursive: true }).filter(f => !f.startsWith('.')).length; } catch { return 0; }
|
|
35
31
|
}
|