atris 3.15.52 → 3.15.53

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.
@@ -79,17 +79,20 @@ cp atris/features/_templates/validate.md.template atris/features/your-feature-na
79
79
 
80
80
  ### Active Features
81
81
 
82
- #### audit-gaps
83
- Close remaining audit gaps from self-audit
84
- - **Files:** atris/team/*.md, atris/features/README.md
85
- - **Status:** complete
86
- - **Keywords:** audit, persona, cleanup
87
- - **What:** Add PERSONA.md reference to all 5 agent specs, clean up stale feature statuses
82
+ None.
88
83
 
89
84
  ---
90
85
 
91
86
  ### Completed Features
92
87
 
88
+ #### audit-gaps
89
+ Close remaining audit gaps from self-audit
90
+ - **Files:** atris/team/*/MEMBER.md, atris/features/README.md, atris/features/audit-gaps/*
91
+ - **Status:** complete
92
+ - **Keywords:** audit, persona, cleanup
93
+ - **What:** Agent member specs reference PERSONA.md for communication style, and stale feature statuses are cleaned up
94
+ - **Completed:** 2026-05-19
95
+
93
96
  #### endstate
94
97
  Public benchmark for proving a coordinated stack beats a pinned single-model baseline
95
98
  - **Files:** atris/features/endstate/*, commands/autopilot.js, commands/experiments.js, commands/loop.js, lib/wiki.js
@@ -116,7 +119,7 @@ Local-first project wiki with cloud opt-in
116
119
 
117
120
  #### self-improving-loop
118
121
  Make Atris recursive — validate.md lessons feed back into the next idea.md
119
- - **Files:** atris/lessons.md (new), atris.md, atris/team/navigator.md, atris/team/validator.md, atris/MAP.md
122
+ - **Files:** atris/lessons.md (new), atris.md, atris/team/navigator/MEMBER.md, atris/team/validator/MEMBER.md, atris/MAP.md
120
123
  - **Status:** complete
121
124
  - **Keywords:** recursion, lessons, feedback-loop, self-improving, lessons.md
122
125
  - **What:** lessons.md accumulates validated learnings; navigator reads them before planning; validator harvests them after validating
@@ -1260,6 +1260,18 @@ function formatWorkspaceRef(workspace) {
1260
1260
  return workspace.name ? `${workspace.name} (${workspace.id})` : workspace.id;
1261
1261
  }
1262
1262
 
1263
+ function workspaceMatchesInput(workspace, input) {
1264
+ if (!workspace || !input) return false;
1265
+ const wanted = String(input).trim().toLowerCase();
1266
+ if (!wanted) return false;
1267
+ return String(workspace.id || '').toLowerCase() === wanted
1268
+ || String(workspace.name || '').toLowerCase() === wanted;
1269
+ }
1270
+
1271
+ function resolveWorkspaceFromList(workspaces, input) {
1272
+ return (workspaces || []).find((workspace) => workspaceMatchesInput(workspace, input)) || null;
1273
+ }
1274
+
1263
1275
  function formatLeaseAge(seconds) {
1264
1276
  const value = Number(seconds);
1265
1277
  if (!Number.isFinite(value) || value < 0) return '-';
@@ -1519,7 +1531,11 @@ async function computerStatus(token, ctx = null) {
1519
1531
  if (d.endpoint) console.log(` Endpoint: ${d.endpoint}`);
1520
1532
  const workspaces = await listBusinessWorkspaces(token, ctx);
1521
1533
  const defaultWorkspace = workspaces.find((workspace) => workspace.is_default);
1522
- const targetWorkspace = workspaces.find((workspace) => workspace.id === ctx.workspaceId) || (ctx.workspaceId ? { id: ctx.workspaceId } : null);
1534
+ const resolvedTargetWorkspace = resolveWorkspaceFromList(workspaces, ctx.workspaceId);
1535
+ const targetWorkspace = resolvedTargetWorkspace || (ctx.workspaceId ? { id: ctx.workspaceId } : null);
1536
+ const probeCtx = resolvedTargetWorkspace?.id
1537
+ ? { ...ctx, workspaceId: resolvedTargetWorkspace.id }
1538
+ : ctx;
1523
1539
  console.log(` Default workspace: ${formatWorkspaceRef(defaultWorkspace)}`);
1524
1540
  console.log(` Target workspace: ${formatWorkspaceRef(targetWorkspace)}`);
1525
1541
  const attachedFromStatus = d.attached_workspace_id
@@ -1534,8 +1550,8 @@ async function computerStatus(token, ctx = null) {
1534
1550
  console.log(` Lease age: ${formatLeaseAge(d.lease_age_seconds)}`);
1535
1551
  if (d.takeover_hint) console.log(` Takeover hint: ${d.takeover_hint}`);
1536
1552
  }
1537
- if (status === 'running' && d.endpoint && ctx.workspaceId) {
1538
- const attached = await probeAttachedWorkspace(token, ctx);
1553
+ if (status === 'running' && d.endpoint && probeCtx.workspaceId) {
1554
+ const attached = await probeAttachedWorkspace(token, probeCtx);
1539
1555
  if (!attachedFromStatus) {
1540
1556
  const attachedWorkspace = workspaces.find((workspace) => workspace.id === attached.workspaceId) || (attached.workspaceId ? { id: attached.workspaceId } : null);
1541
1557
  console.log(` Attached workspace: ${formatWorkspaceRef(attachedWorkspace)}`);
@@ -748,12 +748,18 @@ function missionVerifierPassed(mission) {
748
748
 
749
749
  function missionDueAt(mission, now = new Date()) {
750
750
  const cadenceSeconds = parseCadenceSeconds(mission.cadence);
751
- if (!mission.last_tick_at || cadenceSeconds === 0) return true;
751
+ if (!mission.last_tick_at) return true;
752
+ if (cadenceSeconds === 0) return !(mission.always_on && missionVerifierPassed(mission));
752
753
  const lastTickAt = Date.parse(mission.last_tick_at);
753
754
  if (!Number.isFinite(lastTickAt)) return true;
754
755
  return now.getTime() - lastTickAt >= cadenceSeconds * 1000;
755
756
  }
756
757
 
758
+ function missionSelectableForLoop(mission, now = new Date()) {
759
+ return missionIsRunnable(mission)
760
+ && !(mission.always_on && missionVerifierPassed(mission) && !missionDueAt(mission, now));
761
+ }
762
+
757
763
  function secondsUntilMissionDue(mission, now = new Date()) {
758
764
  const cadenceSeconds = parseCadenceSeconds(mission?.cadence);
759
765
  if (!mission || !mission.last_tick_at || cadenceSeconds === 0) return 0;
@@ -780,7 +786,7 @@ function missionSortTime(mission) {
780
786
 
781
787
  function selectDueMission(root = process.cwd(), now = new Date()) {
782
788
  const candidates = listMissions(root)
783
- .filter(missionIsRunnable)
789
+ .filter((mission) => missionSelectableForLoop(mission, now))
784
790
  .filter((mission) => mission.verifier)
785
791
  .filter((mission) => mission.always_on || !missionVerifierPassed(mission))
786
792
  .filter((mission) => missionDueAt(mission, now));
@@ -799,7 +805,7 @@ function selectDueMission(root = process.cwd(), now = new Date()) {
799
805
 
800
806
  function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
801
807
  const candidates = listMissions(root)
802
- .filter(missionIsRunnable);
808
+ .filter((mission) => missionSelectableForLoop(mission, now));
803
809
 
804
810
  candidates.sort((a, b) => {
805
811
  const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
package/commands/radar.js CHANGED
@@ -432,6 +432,31 @@ function ownerForTask(task) {
432
432
  return task.assigned_to || task.claimed_by || task.metadata?.assigned_to || '-';
433
433
  }
434
434
 
435
+ function taskSessionReason(task) {
436
+ if (!task) return null;
437
+ if (task.status === 'review') return task.metadata?.agent_certified ? 'certified review' : 'review task';
438
+ if (ownerActionRequired(task)) return 'owner action required';
439
+ return null;
440
+ }
441
+
442
+ function taskSessionAction(agent, task, taskWorkspaceRoot) {
443
+ if (!task) return null;
444
+ const ref = taskRef(task);
445
+ const pid = agent?.pid || '?';
446
+ const actor = agent?.agent || 'agent';
447
+ const reviewCommand = taskWorkspaceRoot
448
+ ? `cd ${shellQuote(taskWorkspaceRoot)} && atris task reviews --limit 5`
449
+ : 'atris task reviews --limit 5';
450
+ if (task.status === 'review') {
451
+ if (task.metadata?.agent_certified) {
452
+ return `handoff complete for ${ref}; close pid ${pid} or claim fresh work as ${actor}`;
453
+ }
454
+ return `review or hand off ${ref}: ${reviewCommand}`;
455
+ }
456
+ if (ownerActionRequired(task)) return `owner-gated ${ref}; close pid ${pid} or wait for owner action`;
457
+ return null;
458
+ }
459
+
435
460
  function summarize(tasks, missions, worktrees, agents) {
436
461
  const count = (rows, pred) => rows.filter(pred).length;
437
462
  return {
@@ -505,7 +530,7 @@ function collectRadar(options = {}) {
505
530
  const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
506
531
  const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
507
532
  const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot);
508
- const taskReason = task ? null : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
533
+ const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
509
534
  return {
510
535
  ...agent,
511
536
  task: taskRef(task),
@@ -513,7 +538,7 @@ function collectRadar(options = {}) {
513
538
  owner: ownerForTask(task),
514
539
  task_workspace: taskWorkspaceRoot ? repoLabel(taskWorkspaceRoot) : null,
515
540
  task_reason: taskReason,
516
- task_action: task ? null : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
541
+ task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
517
542
  };
518
543
  });
519
544
  const osState = {
@@ -624,6 +649,11 @@ function sortedAgents(agents = []) {
624
649
  function agentProcessNextAction(agents = [], fallback = 'no obvious process action') {
625
650
  const stopped = agents.filter(agent => agent.status !== 'active').length;
626
651
  if (stopped > 0) return `inspect ${stopped} stopped agent session${stopped === 1 ? '' : 's'}`;
652
+ const ownerGated = agents.filter(agent => agent.task_reason === 'owner action required');
653
+ if (ownerGated.length > 0) {
654
+ 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`;
656
+ }
627
657
  const taskLoad = summarizeTaskLoad(agents);
628
658
  const reviewBound = taskLoad.find(row => row.status.split(/,\s*/).includes('review'));
629
659
  if (reviewBound) return `close or hand off ${reviewBound.sessions} session${reviewBound.sessions === 1 ? '' : 's'} still bound to review task ${reviewBound.task}`;
@@ -744,6 +774,22 @@ function renderAgentTop(data) {
744
774
  lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'}: ${agent.task_reason || 'unmapped'} -> ${agent.task_action || 'inspect session'}`);
745
775
  }
746
776
  }
777
+ const ownerGatedAgents = payload.agents.filter(row => row.task_reason === 'owner action required').slice(0, 8);
778
+ if (ownerGatedAgents.length) {
779
+ lines.push('');
780
+ lines.push(`Owner-gated: ${ownerGatedAgents.length} session${ownerGatedAgents.length === 1 ? '' : 's'} waiting on owner-only tasks.`);
781
+ for (const agent of ownerGatedAgents) {
782
+ lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'} ${agent.task}: ${agent.task_action || 'wait for owner action'}`);
783
+ }
784
+ }
785
+ const reviewBoundAgents = payload.agents.filter(row => row.task_status === 'review').slice(0, 8);
786
+ if (reviewBoundAgents.length) {
787
+ 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) {
790
+ lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'} ${agent.task}: ${agent.task_reason || 'review'} -> ${agent.task_action || 'close or hand off session'}`);
791
+ }
792
+ }
747
793
  const taskLoadRows = payload.task_load.filter(row => row.attention).slice(0, 8);
748
794
  if (taskLoadRows.length) {
749
795
  lines.push('');
@@ -285,6 +285,12 @@ function createOrFindPr(root, branch, targetRef, title, dryRun) {
285
285
  return created.stdout.trim();
286
286
  }
287
287
 
288
+ function prMergeRef(prOutput) {
289
+ const text = String(prOutput || '').trim();
290
+ if (!text || text.startsWith('dry-run:')) return '';
291
+ return text.split(/\s+/)[0];
292
+ }
293
+
288
294
  function shipWorktree(args) {
289
295
  const root = repoRoot();
290
296
  const dryRun = hasFlag(args, '--dry-run');
@@ -359,8 +365,20 @@ function shipWorktree(args) {
359
365
  if (merge) {
360
366
  console.log('merge: requested');
361
367
  if (!dryRun) {
362
- const merged = spawnSync('gh', ['pr', 'merge', '--merge', '--delete-branch'], { cwd: root, encoding: 'utf8' });
368
+ const mergeRef = prMergeRef(pr);
369
+ const mergeArgs = ['pr', 'merge'];
370
+ if (mergeRef) mergeArgs.push(mergeRef);
371
+ mergeArgs.push('--merge');
372
+ const merged = spawnSync('gh', mergeArgs, { cwd: root, encoding: 'utf8' });
363
373
  if (merged.status !== 0) throw new Error((merged.stderr || merged.stdout || 'gh pr merge failed').trim());
374
+ console.log('merge: merged');
375
+ const deleted = runGit(['push', 'origin', '--delete', branch], { cwd: root, check: false });
376
+ if (deleted.status === 0) {
377
+ console.log(`merge: remote branch deleted ${branch}`);
378
+ } else {
379
+ const deleteOutput = (deleted.stderr || deleted.stdout || 'remote branch delete failed').trim();
380
+ console.log(`merge: remote branch delete skipped: ${deleteOutput}`);
381
+ }
364
382
  }
365
383
  }
366
384
  } else {
@@ -461,6 +479,7 @@ module.exports = {
461
479
  defaultWorktreePath,
462
480
  parseWorktrees,
463
481
  normalizeTargetRef,
482
+ prMergeRef,
464
483
  slugify,
465
484
  statusCounts,
466
485
  swarloClaim,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.15.52",
3
+ "version": "3.15.53",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",