atris 3.15.51 → 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
@@ -909,6 +909,7 @@ function renderBusinessShareHandoff(state, options = {}) {
909
909
  `Local path: ${workspacePath}`,
910
910
  `Ready to share: ${state.ready ? 'yes' : 'no'}`,
911
911
  `Remote pull: ${state.remoteReady ? 'available' : 'local-only'}`,
912
+ `Agent setup: ${(state.missingRootAgentAdapters && state.missingRootAgentAdapters.length) ? 'missing root agent adapters' : 'ready'}`,
912
913
  '',
913
914
  '## Get The Workspace',
914
915
  '',
@@ -946,9 +947,9 @@ function renderBusinessShareHandoff(state, options = {}) {
946
947
  '',
947
948
  '## What To Read',
948
949
  '',
950
+ `- Protocol: atris/atris.md`,
949
951
  `- Map: atris/MAP.md`,
950
952
  `- Queue: atris/TODO.md`,
951
- `- Agent adapters: ${state.rootAgentAdapters && state.rootAgentAdapters.length ? state.rootAgentAdapters.join(', ') : 'missing'}`,
952
953
  `- Team start: ${state.teamStart || 'missing'}`,
953
954
  `- Starter brief: ${state.starterBrief || 'missing'}`,
954
955
  `- First loop: ${state.firstLoop || 'missing'}`,
@@ -1014,11 +1015,12 @@ function renderBusinessStartCard(state, options = {}) {
1014
1015
  `Workspace: ${workspacePath}`,
1015
1016
  `Ready: ${state.ready ? 'yes' : 'no'}`,
1016
1017
  `Remote pull: ${state.remoteReady ? 'available' : 'local-only'}`,
1018
+ `Agent setup: ${(state.missingRootAgentAdapters && state.missingRootAgentAdapters.length) ? 'missing root agent adapters' : 'ready'}`,
1017
1019
  '',
1018
1020
  'Read:',
1021
+ `- atris/atris.md`,
1019
1022
  `- atris/MAP.md`,
1020
1023
  `- atris/TODO.md`,
1021
- `- ${(state.rootAgentAdapters && state.rootAgentAdapters.length === 3) ? state.rootAgentAdapters.join(', ') : 'missing root agent adapters'}`,
1022
1024
  `- ${state.teamStart || 'missing team start guide'}`,
1023
1025
  `- ${state.starterBrief || 'missing starter brief'}`,
1024
1026
  `- ${state.firstLoop || 'missing first loop'}`,
@@ -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;
@@ -763,8 +769,15 @@ function secondsUntilMissionDue(mission, now = new Date()) {
763
769
  return Math.max(0, Math.ceil((dueAt - now.getTime()) / 1000));
764
770
  }
765
771
 
772
+ function missionHasHumanAsks(mission) {
773
+ return Array.isArray(mission?.human_asks)
774
+ && mission.human_asks.some((ask) => String(ask || '').trim());
775
+ }
776
+
766
777
  function missionIsRunnable(mission) {
767
- return mission && GOAL_LOOP_STATUSES.has(String(mission.status || ''));
778
+ return mission
779
+ && GOAL_LOOP_STATUSES.has(String(mission.status || ''))
780
+ && !missionHasHumanAsks(mission);
768
781
  }
769
782
 
770
783
  function missionSortTime(mission) {
@@ -773,7 +786,7 @@ function missionSortTime(mission) {
773
786
 
774
787
  function selectDueMission(root = process.cwd(), now = new Date()) {
775
788
  const candidates = listMissions(root)
776
- .filter(missionIsRunnable)
789
+ .filter((mission) => missionSelectableForLoop(mission, now))
777
790
  .filter((mission) => mission.verifier)
778
791
  .filter((mission) => mission.always_on || !missionVerifierPassed(mission))
779
792
  .filter((mission) => missionDueAt(mission, now));
@@ -792,7 +805,7 @@ function selectDueMission(root = process.cwd(), now = new Date()) {
792
805
 
793
806
  function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
794
807
  const candidates = listMissions(root)
795
- .filter(missionIsRunnable);
808
+ .filter((mission) => missionSelectableForLoop(mission, now));
796
809
 
797
810
  candidates.sort((a, b) => {
798
811
  const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
@@ -1176,7 +1189,8 @@ function spawnClaudeTick(mission, opts) {
1176
1189
  exitCode: code,
1177
1190
  sessionIds: Array.from(observedSessionIds),
1178
1191
  result: finalText,
1179
- summary: (finalText || '').split('\n').filter(Boolean)[0]?.slice(0, 240) || (ok ? 'no-text' : 'error'),
1192
+ summary: usefulClaudeReceiptSummary(finalText, ok ? 'no-text' : 'error'),
1193
+ receipt_text: cappedClaudeReceiptText(finalText),
1180
1194
  api_equivalent_estimate: costEstimate,
1181
1195
  duration_api_ms: durationApiMs,
1182
1196
  duration_total_ms: Date.now() - startedAt,
@@ -1196,6 +1210,33 @@ function spawnClaudeTick(mission, opts) {
1196
1210
  });
1197
1211
  }
1198
1212
 
1213
+ function stripClaudeReceiptLine(line) {
1214
+ return String(line || '')
1215
+ .trim()
1216
+ .replace(/^#{1,6}\s+/, '')
1217
+ .replace(/^[-*]\s+/, '')
1218
+ .replace(/^\d+[.)]\s+/, '')
1219
+ .trim();
1220
+ }
1221
+
1222
+ function usefulClaudeReceiptSummary(text, fallback = 'no-text') {
1223
+ const lines = String(text || '').split(/\r?\n/);
1224
+ for (const line of lines) {
1225
+ const clean = stripClaudeReceiptLine(line);
1226
+ if (!clean) continue;
1227
+ if (/^(receipt|summary|final|final answer|result)$/i.test(clean)) continue;
1228
+ return clean.slice(0, 240);
1229
+ }
1230
+ return fallback;
1231
+ }
1232
+
1233
+ function cappedClaudeReceiptText(text, limit = 4000) {
1234
+ const clean = String(text || '').trim();
1235
+ if (!clean) return '';
1236
+ if (clean.length <= limit) return clean;
1237
+ return clean.slice(0, limit - 16).trimEnd() + '\n...[truncated]';
1238
+ }
1239
+
1199
1240
  async function runMission(args) {
1200
1241
  const asJson = wantsJson(args);
1201
1242
  const dueMode = hasFlag(args, '--due');
@@ -1323,7 +1364,7 @@ async function runMission(args) {
1323
1364
  if (mission.verifier !== frozen.verifier) { pauseReason = 'verifier-mutated'; break; }
1324
1365
  if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
1325
1366
 
1326
- const tickIdx = ticks.length + 1;
1367
+ const tickIdx = Number(mission.last_tick_index || 0) + 1;
1327
1368
  const tickStart = stampIso();
1328
1369
  const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
1329
1370
  let result = { status: 'skipped', reason: 'unknown', tick_index: tickIdx, ran: false, started_at: tickStart };
@@ -1358,6 +1399,7 @@ async function runMission(args) {
1358
1399
  result.claude = {
1359
1400
  ok: claudeResult.ok,
1360
1401
  summary: claudeResult.summary,
1402
+ receipt_text: claudeResult.receipt_text,
1361
1403
  stop_reason: claudeResult.stop_reason,
1362
1404
  api_equivalent_estimate: claudeResult.api_equivalent_estimate,
1363
1405
  duration_total_ms: claudeResult.duration_total_ms,
@@ -1424,7 +1466,7 @@ async function runMission(args) {
1424
1466
  });
1425
1467
 
1426
1468
  const xpReadyAction = missionXpReadyAction(mission, receiptPath);
1427
- const newStatus = (verifierResult?.passed && mission.always_on) ? 'running' :
1469
+ const newStatus = (verifierResult?.passed && mission.always_on) ? 'ready' :
1428
1470
  (verifierResult?.passed && xpReadyAction) ? 'ready' :
1429
1471
  (verifierResult?.passed && completeOnPass) ? 'complete' :
1430
1472
  (verifierResult?.passed ? 'ready' :
@@ -1448,6 +1490,7 @@ async function runMission(args) {
1448
1490
  last_tick_at: finishedAt,
1449
1491
  last_tick_status: result.status,
1450
1492
  last_tick_reason: result.reason,
1493
+ last_tick_index: tickIdx,
1451
1494
  verifier_result: verifierResult || mission.verifier_result || null,
1452
1495
  receipt_path: receiptPath,
1453
1496
  next_action: nextAction,
@@ -1915,4 +1958,6 @@ module.exports = {
1915
1958
  renderMissionStatus,
1916
1959
  selectDueMission,
1917
1960
  selectCodexGoalMission,
1961
+ usefulClaudeReceiptSummary,
1962
+ cappedClaudeReceiptText,
1918
1963
  };
package/commands/play.js CHANGED
@@ -327,7 +327,7 @@ function playWorkspaceRoot(taskDb, workspaceArg) {
327
327
  }
328
328
 
329
329
  function nextCommands(task, player) {
330
- const helper = 'game-manager';
330
+ const helper = player || 'player';
331
331
  if (!task) {
332
332
  return [
333
333
  `atris task delegate "AgentXP first rep: one proof-backed mission" --to ${player} --tag agent-xp`,
package/commands/radar.js CHANGED
@@ -114,18 +114,26 @@ 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
- 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
- }));
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
+ });
129
137
  }
130
138
 
131
139
  function findTaskWorkspaceRoot(cwd, deps) {
@@ -424,6 +432,31 @@ function ownerForTask(task) {
424
432
  return task.assigned_to || task.claimed_by || task.metadata?.assigned_to || '-';
425
433
  }
426
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
+
427
460
  function summarize(tasks, missions, worktrees, agents) {
428
461
  const count = (rows, pred) => rows.filter(pred).length;
429
462
  return {
@@ -439,9 +472,31 @@ function summarize(tasks, missions, worktrees, agents) {
439
472
  };
440
473
  }
441
474
 
475
+ function ownerActionRequired(task) {
476
+ const metadata = task?.metadata || {};
477
+ if (metadata.owner_action_required === true || metadata.agent_executable === false) return true;
478
+ if (String(metadata.agent_executable || '').toLowerCase() === 'false') return true;
479
+ const text = [
480
+ task?.title,
481
+ task?.tag,
482
+ metadata.status,
483
+ metadata.blocker,
484
+ metadata.human_revision_note,
485
+ metadata.latest_agent_proof,
486
+ metadata.latest_agent_lesson,
487
+ task?.routing_text,
488
+ ].filter(Boolean).join(' ').toLowerCase();
489
+ if (text.includes('owner_action_required') || text.includes('agent_executable=false')) return true;
490
+ if (text.includes('owner-only') || text.includes('owner only') || text.includes('not agent-executable')) return true;
491
+ return text.includes('owner') && text.includes('billing') && (text.includes('spending limit') || text.includes('failed payments'));
492
+ }
493
+
442
494
  function nextAction(tasks, missions, worktrees, agents, os = {}) {
443
- const activeTask = tasks.find(t => t.status === 'claimed' || t.status === 'open');
495
+ const activeTasks = tasks.filter(t => t.status === 'claimed' || t.status === 'open');
496
+ const activeTask = activeTasks.find(t => !ownerActionRequired(t));
444
497
  if (activeTask) return `work ${taskRef(activeTask)}: ${activeTask.title || 'active task'}`;
498
+ const ownerTask = activeTasks.find(ownerActionRequired);
499
+ if (ownerTask) return `owner action required ${taskRef(ownerTask)}: ${ownerTask.title || 'owner-only task'}`;
445
500
  const needsReview = tasks.find(t => t.status === 'review' && !(t.metadata && t.metadata.agent_certified));
446
501
  if (needsReview) return `review ${taskRef(needsReview)}: ${needsReview.title || 'uncertified review task'}`;
447
502
  const certified = tasks.find(t => t.status === 'review' && t.metadata && t.metadata.agent_certified);
@@ -475,7 +530,7 @@ function collectRadar(options = {}) {
475
530
  const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
476
531
  const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
477
532
  const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot);
478
- const taskReason = task ? null : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
533
+ const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
479
534
  return {
480
535
  ...agent,
481
536
  task: taskRef(task),
@@ -483,7 +538,7 @@ function collectRadar(options = {}) {
483
538
  owner: ownerForTask(task),
484
539
  task_workspace: taskWorkspaceRoot ? repoLabel(taskWorkspaceRoot) : null,
485
540
  task_reason: taskReason,
486
- task_action: task ? null : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
541
+ task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
487
542
  };
488
543
  });
489
544
  const osState = {
@@ -594,6 +649,11 @@ function sortedAgents(agents = []) {
594
649
  function agentProcessNextAction(agents = [], fallback = 'no obvious process action') {
595
650
  const stopped = agents.filter(agent => agent.status !== 'active').length;
596
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
+ }
597
657
  const taskLoad = summarizeTaskLoad(agents);
598
658
  const reviewBound = taskLoad.find(row => row.status.split(/,\s*/).includes('review'));
599
659
  if (reviewBound) return `close or hand off ${reviewBound.sessions} session${reviewBound.sessions === 1 ? '' : 's'} still bound to review task ${reviewBound.task}`;
@@ -714,6 +774,22 @@ function renderAgentTop(data) {
714
774
  lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'}: ${agent.task_reason || 'unmapped'} -> ${agent.task_action || 'inspect session'}`);
715
775
  }
716
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
+ }
717
793
  const taskLoadRows = payload.task_load.filter(row => row.attention).slice(0, 8);
718
794
  if (taskLoadRows.length) {
719
795
  lines.push('');
package/commands/task.js CHANGED
@@ -1708,7 +1708,7 @@ function cmdReady(args) {
1708
1708
  next_action: agentCertified ? 'continue_work' : 'agent_review_again',
1709
1709
  rule: agentCertified
1710
1710
  ? 'Agent double-check complete; continue work. AgentXP waits for human accept.'
1711
- : 'Proof is in Review; one more agent review pass certifies continuation. AgentXP waits for human accept.',
1711
+ : 'Proof is in Review; human accept can award AgentXP now. A second agent review only certifies autonomous continuation.',
1712
1712
  };
1713
1713
  if (wantsJson(args)) {
1714
1714
  printJson({
@@ -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.51",
3
+ "version": "3.15.53",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",