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.
Files changed (44) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +11 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +32 -31
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +197 -22
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/computer.js +45 -83
  18. package/commands/improve.js +501 -0
  19. package/commands/integrations.js +228 -0
  20. package/commands/lesson.js +44 -0
  21. package/commands/member.js +4498 -226
  22. package/commands/mission.js +302 -27
  23. package/commands/now.js +89 -1
  24. package/commands/radar.js +181 -56
  25. package/commands/skill.js +37 -6
  26. package/commands/soul.js +0 -4
  27. package/commands/task.js +5582 -517
  28. package/commands/terminal.js +14 -10
  29. package/commands/wiki.js +87 -1
  30. package/commands/workflow.js +288 -73
  31. package/commands/worktree.js +52 -15
  32. package/commands/xp.js +41 -65
  33. package/lib/auto-accept-certified.js +294 -0
  34. package/lib/file-ops.js +0 -184
  35. package/lib/member-alive.js +232 -0
  36. package/lib/policy-lessons.js +280 -0
  37. package/lib/receipt-evidence.js +64 -0
  38. package/lib/state-detection.js +34 -0
  39. package/lib/task-db.js +568 -16
  40. package/lib/task-proof.js +43 -0
  41. package/package.json +1 -1
  42. package/utils/auth.js +13 -4
  43. package/commands/research.js +0 -52
  44. 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 completedCount = countJournalCompletedReceipts(journalPath);
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
- const messages = Array.isArray(task.messages) ? task.messages : [];
119
- const row = {
120
- id: task.id,
121
- display_id: task.display_id,
122
- legacy_ref: task.legacy_ref,
123
- title: task.title,
124
- status: task.status,
125
- tag: task.tag,
126
- workspace_root: task.workspace_root,
127
- claimed_by: task.claimed_by,
128
- assigned_to: task.metadata?.assigned_to || task.assigned_to || null,
129
- metadata: task.metadata || {},
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 taskForCwd(tasks, cwd, workspaceRoot = cwd) {
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
- return tasks.find(task => matchesWorkspace(task) && task.status === 'claimed')
425
- || tasks.find(task => matchesWorkspace(task) && task.status === 'open')
426
- || tasks.find(task => matchesWorkspace(task) && task.status === 'review')
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}; close pid ${pid} or claim fresh work as ${actor}`;
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} or wait for owner action`;
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
- return `owner gate blocks ${ownerGated.length} session${ownerGated.length === 1 ? '' : 's'}${tasks ? ` on ${tasks}` : ''}; wait for owner action or close idle sessions`;
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) return `close or hand off ${reviewBound.sessions} session${reviewBound.sessions === 1 ? '' : 's'} still bound to review task ${reviewBound.task}`;
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) return `inspect ${pileup.sessions} sessions on ${pileup.task} (${pileup.cpu.toFixed(1)}% CPU)`;
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 ownerGatedAgents = payload.agents.filter(row => row.task_reason === 'owner action required').slice(0, 8);
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
- lines.push(`Owner-gated: ${ownerGatedAgents.length} session${ownerGatedAgents.length === 1 ? '' : 's'} waiting on owner-only tasks.`);
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 reviewBoundAgents = payload.agents.filter(row => row.task_status === 'review').slice(0, 8);
786
- if (reviewBoundAgents.length) {
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
- lines.push(`Review-bound: ${reviewBoundAgents.length} session${reviewBoundAgents.length === 1 ? '' : 's'} still tied to Review tasks.`);
789
- for (const agent of reviewBoundAgents) {
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
- lines.push(`- ${row.task}: ${row.sessions} sessions, ${row.cpu.toFixed(1)}% CPU, ${row.status}, ${repoText}`);
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 skillsDir = path.join(process.cwd(), 'atris', 'skills');
359
- const skills = findAllSkills(skillsDir);
391
+ const skills = findReadableSkills();
360
392
 
361
393
  if (skills.length === 0) {
362
- console.log('No skills found in atris/skills/. Run "atris init" first.');
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 skillsDir = path.join(process.cwd(), 'atris', 'skills');
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
  }