atris 3.15.57 → 3.16.1

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 (47) 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 +12 -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 +78 -44
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +628 -31
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/compile.js +569 -0
  18. package/commands/computer.js +0 -60
  19. package/commands/improve.js +501 -0
  20. package/commands/integrations.js +233 -71
  21. package/commands/lesson.js +44 -0
  22. package/commands/member.js +4498 -226
  23. package/commands/mission.js +302 -27
  24. package/commands/now.js +89 -1
  25. package/commands/probe.js +366 -0
  26. package/commands/radar.js +181 -56
  27. package/commands/recap.js +203 -0
  28. package/commands/skill.js +6 -2
  29. package/commands/soul.js +0 -4
  30. package/commands/task.js +5587 -499
  31. package/commands/terminal.js +14 -10
  32. package/commands/wiki.js +87 -1
  33. package/commands/workflow.js +288 -73
  34. package/commands/worktree.js +52 -15
  35. package/commands/xp.js +6 -65
  36. package/lib/auto-accept-certified.js +294 -0
  37. package/lib/file-ops.js +0 -184
  38. package/lib/member-alive.js +232 -0
  39. package/lib/policy-lessons.js +280 -0
  40. package/lib/receipt-evidence.js +64 -0
  41. package/lib/state-detection.js +75 -1
  42. package/lib/task-db.js +568 -16
  43. package/lib/task-proof.js +43 -0
  44. package/package.json +1 -1
  45. package/utils/auth.js +13 -4
  46. package/commands/research.js +0 -52
  47. package/lib/section-merge.js +0 -196
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');
@@ -0,0 +1,203 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DAY_MS = 24 * 60 * 60 * 1000;
5
+ const DEFAULT_DAYS = 7;
6
+
7
+ function loadTaskDb() {
8
+ try {
9
+ return require('../lib/task-db');
10
+ } catch (e) {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function readProjection(root) {
16
+ const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
17
+ if (!fs.existsSync(projectionPath)) return null;
18
+ try {
19
+ const parsed = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
20
+ return Array.isArray(parsed.tasks) ? parsed.tasks : null;
21
+ } catch (e) {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function loadTasks(root) {
27
+ const taskDb = loadTaskDb();
28
+ if (taskDb) {
29
+ try {
30
+ const db = taskDb.open();
31
+ const ws = taskDb.workspaceRoot(root);
32
+ const rows = taskDb.listTasks(db, { workspaceRoot: ws });
33
+ const refs = taskDb.taskDisplayRefMap(rows);
34
+ return rows.map(row => ({ ...row, display_id: refs.get(row.id) || row.id.slice(-6) }));
35
+ } catch (e) {
36
+ // fall through to projection
37
+ }
38
+ }
39
+ return readProjection(root);
40
+ }
41
+
42
+ function taskProof(task) {
43
+ const meta = task.metadata || {};
44
+ const proof = String(meta.latest_agent_proof || '').trim();
45
+ if (proof) return proof;
46
+ if (meta.agent_certified === true) return 'verified by repeated agent review';
47
+ return null;
48
+ }
49
+
50
+ function shortProof(proof, width = 70) {
51
+ if (!proof) return null;
52
+ const flat = proof.replace(/\s+/g, ' ').trim();
53
+ return flat.length <= width ? flat : `${flat.slice(0, width - 1)}…`;
54
+ }
55
+
56
+ function shortTitle(title, width = 64) {
57
+ const flat = String(title || '').replace(/\s+/g, ' ').trim();
58
+ return flat.length <= width ? flat : `${flat.slice(0, width - 1)}…`;
59
+ }
60
+
61
+ function buildRecapData(root = process.cwd(), { days = DEFAULT_DAYS } = {}) {
62
+ const windowDays = Number.isFinite(Number(days)) && Number(days) > 0 ? Number(days) : DEFAULT_DAYS;
63
+ const tasks = loadTasks(root);
64
+ if (!tasks || tasks.length === 0) return { empty: true, days: windowDays, workspace: path.basename(root) };
65
+
66
+ const cutoff = Date.now() - windowDays * DAY_MS;
67
+ const pick = t => ({
68
+ id: t.display_id || t.id,
69
+ title: String(t.title || '').trim(),
70
+ proof: taskProof(t),
71
+ owner: t.claimed_by || (t.metadata && t.metadata.assigned_to) || null,
72
+ done_at: t.done_at || null,
73
+ });
74
+
75
+ const shipped = tasks
76
+ .filter(t => t.status === 'done' && Number(t.done_at || 0) >= cutoff)
77
+ .sort((a, b) => Number(b.done_at || 0) - Number(a.done_at || 0))
78
+ .map(pick);
79
+ const waiting = tasks
80
+ .filter(t => t.status === 'review')
81
+ .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
82
+ .map(pick);
83
+ const inProgress = tasks
84
+ .filter(t => t.status === 'open' || t.status === 'claimed')
85
+ .map(pick);
86
+
87
+ const withProof = [...shipped, ...waiting].filter(t => t.proof).length;
88
+ return {
89
+ empty: false,
90
+ days: windowDays,
91
+ workspace: path.basename(root),
92
+ shipped,
93
+ waiting,
94
+ inProgress,
95
+ proof_attached: withProof,
96
+ proof_total: shipped.length + waiting.length,
97
+ };
98
+ }
99
+
100
+ function renderRecap(data) {
101
+ if (data.empty) {
102
+ return [
103
+ `RECAP — ${data.workspace}`,
104
+ '',
105
+ 'No task history yet.',
106
+ 'Run "atris init", then let an agent work — every finished task lands here with proof.',
107
+ ].join('\n');
108
+ }
109
+ const lines = [];
110
+ lines.push(`RECAP — ${data.workspace} — last ${data.days} day${data.days === 1 ? '' : 's'}`);
111
+ lines.push('');
112
+ const headline = [];
113
+ if (data.shipped.length) headline.push(`${data.shipped.length} change${data.shipped.length === 1 ? '' : 's'} shipped`);
114
+ if (data.waiting.length) headline.push(`${data.waiting.length} finished and waiting for your sign-off`);
115
+ if (data.inProgress.length) headline.push(`${data.inProgress.length} in progress`);
116
+ lines.push(headline.length ? `Your AI team: ${headline.join(' · ')}.` : 'Quiet window — no movement in this period.');
117
+ lines.push('Every finished line below carries proof: the commands run and their results.');
118
+
119
+ if (data.shipped.length) {
120
+ lines.push('');
121
+ lines.push(`SHIPPED (accepted by a human) — ${data.shipped.length}`);
122
+ for (const t of data.shipped.slice(0, 12)) {
123
+ lines.push(` ${t.id} ${shortTitle(t.title)}`);
124
+ if (t.proof) lines.push(` proof: ${shortProof(t.proof)}`);
125
+ }
126
+ if (data.shipped.length > 12) lines.push(` … and ${data.shipped.length - 12} more, all with proof on file`);
127
+ }
128
+
129
+ if (data.waiting.length) {
130
+ lines.push('');
131
+ lines.push(`FINISHED, WAITING FOR YOUR SIGN-OFF — ${data.waiting.length}`);
132
+ for (const t of data.waiting.slice(0, 10)) {
133
+ lines.push(` ${t.id} ${shortTitle(t.title)}`);
134
+ }
135
+ if (data.waiting.length > 10) lines.push(` … and ${data.waiting.length - 10} more`);
136
+ lines.push(' approve or send back: atris task reviews');
137
+ }
138
+
139
+ if (data.inProgress.length) {
140
+ lines.push('');
141
+ lines.push(`IN PROGRESS — ${data.inProgress.length}`);
142
+ for (const t of data.inProgress) {
143
+ lines.push(` ${t.id} ${shortTitle(t.title)}${t.owner ? ` @${t.owner}` : ''}`);
144
+ }
145
+ }
146
+
147
+ lines.push('');
148
+ lines.push(`Proof attached: ${data.proof_attached}/${data.proof_total} finished items.`);
149
+ lines.push('Paste-ready summary for Slack or email: atris recap --share');
150
+ return lines.join('\n');
151
+ }
152
+
153
+ function renderShare(data) {
154
+ if (data.empty) return `Nothing to share yet on ${data.workspace} — no finished tasks on record.`;
155
+ const lines = [];
156
+ lines.push(`What the AI team did on ${data.workspace} in the last ${data.days} day${data.days === 1 ? '' : 's'}:`);
157
+ lines.push('');
158
+ if (data.shipped.length) lines.push(`- ${data.shipped.length} change${data.shipped.length === 1 ? '' : 's'} shipped, each verified before a human accepted it`);
159
+ if (data.waiting.length) lines.push(`- ${data.waiting.length} more finished with proof attached, waiting for human sign-off`);
160
+ if (data.inProgress.length) lines.push(`- ${data.inProgress.length} task${data.inProgress.length === 1 ? '' : 's'} in progress`);
161
+ const highlights = [...data.shipped, ...data.waiting].filter(t => t.proof).slice(0, 5);
162
+ if (highlights.length) {
163
+ lines.push('');
164
+ lines.push('Highlights:');
165
+ for (const t of highlights) {
166
+ lines.push(`- ${shortTitle(t.title, 80)} (proof: ${shortProof(t.proof, 60)})`);
167
+ }
168
+ }
169
+ lines.push('');
170
+ lines.push('Every item above is backed by a receipt — the exact commands run and their results — not a status update someone typed.');
171
+ return lines.join('\n');
172
+ }
173
+
174
+ function printRecapHelp() {
175
+ console.log(`
176
+ atris recap - what your AI team actually did, in plain English
177
+
178
+ atris recap Last 7 days: shipped, waiting on you, in progress
179
+ atris recap --days 30 Widen the window
180
+ atris recap --share Paste-ready summary for Slack, email, or a customer
181
+ atris recap --json Structured output for agents and dashboards
182
+
183
+ Reads the workspace task records and their proof. No jargon, no guesses:
184
+ if it is listed as finished, the receipt is on file.
185
+ `);
186
+ }
187
+
188
+ function recapAtris(args = []) {
189
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
190
+ printRecapHelp();
191
+ return;
192
+ }
193
+ const daysIdx = args.indexOf('--days');
194
+ const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
195
+ const data = buildRecapData(process.cwd(), { days });
196
+ if (args.includes('--json')) {
197
+ console.log(JSON.stringify(data, null, 2));
198
+ return;
199
+ }
200
+ console.log(args.includes('--share') ? renderShare(data) : renderRecap(data));
201
+ }
202
+
203
+ module.exports = { recapAtris, buildRecapData, renderRecap, renderShare };
package/commands/skill.js CHANGED
@@ -183,8 +183,12 @@ function runAuditChecks(skill) {
183
183
  });
184
184
 
185
185
  // 7. no XML tags in content (skip placeholders like <name>, <keyword>, code blocks)
186
- const xmlMatches = skill.content.match(/<[a-zA-Z][^>]*>/g) || [];
187
- const placeholders = /^<(name|keyword|placeholder|value|type|path|file|dir|id|url|tag|description|your-|user-|project-|skill-)/i;
186
+ const proseContent = skill.content
187
+ .replace(/```[\s\S]*?```/g, '') // fenced code blocks
188
+ .replace(/`[^`\n]*`/g, ''); // inline code spans
189
+ const xmlMatches = proseContent.match(/<[a-zA-Z][^>]*>/g) || [];
190
+ // Single-letter tags like <X>/<N> are prose placeholders, not real XML.
191
+ const placeholders = /^<(name|keyword|placeholder|value|type|path|file|dir|id|url|tag|description|your-|user-|project-|skill-|[a-zA-Z]>)/i;
188
192
  const realXml = xmlMatches.filter(t =>
189
193
  !t.startsWith('<!--') && !t.startsWith('<!') && !placeholders.test(t)
190
194
  );
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
  }