atris 3.32.0 → 3.33.3

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.
@@ -611,16 +611,48 @@ function startMemberRunMission(name, missionText, args = []) {
611
611
  }
612
612
  }
613
613
 
614
- // atris member ping <name> "<message>" — talk to an always-on member mid-run.
615
- // Finds the member's most recently touched live mission (including --worktree
616
- // missions in sibling checkouts) and leaves the note its next tick consumes.
614
+ // atris member ping <name> "<message>" — talk to a busy member mid-run.
615
+ // Two delivery lanes: the member's most recently touched live mission
616
+ // (including --worktree missions in sibling checkouts), whose next tick
617
+ // consumes the note, and the member's claimed task dialogue, which task-loop
618
+ // agents re-read every step without ever ticking a mission. The ping lands on
619
+ // every lane that exists and errors only when neither does.
620
+ function pingClaimedTaskDialogue(name, text, from) {
621
+ let taskDb;
622
+ try {
623
+ taskDb = require('../lib/task-db');
624
+ } catch {
625
+ return null;
626
+ }
627
+ try {
628
+ const db = taskDb.open();
629
+ const local = taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot(), status: 'claimed', claimedBy: name });
630
+ const pool = local.length ? local : taskDb.listTasks(db, { status: 'claimed', claimedBy: name });
631
+ if (!pool.length) return null;
632
+ const task = [...pool].sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))[0];
633
+ const result = taskDb.noteTask(db, { id: task.id, actor: from, content: text });
634
+ if (!result.noted) return null;
635
+ return { task_id: task.id, title: task.title };
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+
617
641
  function memberPing(name, ...args) {
618
642
  if (!name || name === '--help' || name === '-h') {
619
- console.log('Usage: atris member ping <name> "<message>" [--json]');
620
- console.log('Leaves a note on the member\'s active mission; the next tick reads it as operator direction.');
643
+ console.log('Usage: atris member ping <name> "<message>" [--from <who>] [--json]');
644
+ console.log('Leaves the note on the member\'s active mission and claimed task dialogue; whichever loop the member runs reads it next.');
621
645
  return;
622
646
  }
623
- const text = args.filter((a) => !a.startsWith('--')).join(' ').trim();
647
+ const asJson = args.includes('--json');
648
+ const rest = args.filter((a) => a !== '--json');
649
+ let from = process.env.USER || 'operator';
650
+ const fromIdx = rest.indexOf('--from');
651
+ if (fromIdx !== -1) {
652
+ from = rest[fromIdx + 1] || from;
653
+ rest.splice(fromIdx, 2);
654
+ }
655
+ const text = rest.filter((a) => !a.startsWith('--')).join(' ').trim();
624
656
  if (!text) {
625
657
  console.error('atris member ping: message required');
626
658
  process.exit(2);
@@ -633,12 +665,33 @@ function memberPing(name, ...args) {
633
665
  ]
634
666
  .filter((m) => m && m.owner === name && !terminal.has(m.status))
635
667
  .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
636
- if (!candidates.length) {
637
- console.error(`atris member ping: no live mission owned by "${name}". Start one: atris member run ${name} "<objective>"`);
668
+
669
+ const taskNote = pingClaimedTaskDialogue(name, text, from);
670
+
671
+ if (!candidates.length && !taskNote) {
672
+ console.error(`atris member ping: no live mission or claimed task for "${name}". Start one: atris member run ${name} "<objective>"`);
638
673
  process.exit(1);
639
674
  }
640
- const passthrough = args.filter((a) => a.startsWith('--'));
641
- return missionMod.pingMission([candidates[0].id, text, ...passthrough]);
675
+
676
+ const mission = candidates.length
677
+ ? missionMod.pingMission([candidates[0].id, text, '--from', from], { silent: true })
678
+ : null;
679
+ const pendingPings = mission ? (mission.pings || []).filter((p) => p && !p.consumed_at).length : null;
680
+
681
+ if (asJson) {
682
+ console.log(JSON.stringify({
683
+ ok: true,
684
+ action: 'member_ping',
685
+ member: name,
686
+ mission_id: mission ? mission.id : null,
687
+ pending_pings: pendingPings,
688
+ task_id: taskNote ? taskNote.task_id : null,
689
+ }));
690
+ } else {
691
+ if (mission) console.log(`pinged ${mission.id} — the next tick reads it (${pendingPings} unread).`);
692
+ if (taskNote) console.log(`pinged task ${taskNote.task_id} dialogue — ${name} reads it on the next step.`);
693
+ }
694
+ return { mission, task_note: taskNote };
642
695
  }
643
696
 
644
697
  function memberRun(name, ...args) {
@@ -30,6 +30,15 @@ const {
30
30
  formatBytes,
31
31
  } = require('../lib/runs-prune');
32
32
  const { operatorReady, hasAgentJargon } = require('./autoland');
33
+ const {
34
+ MISSION_INSPECT_FIELDS,
35
+ readFieldsFlag,
36
+ stripInspectArgs,
37
+ validateFields,
38
+ missionInspectFieldValues,
39
+ inspectTextLines,
40
+ buildInspectPayload,
41
+ } = require('../lib/inspect-fields');
33
42
 
34
43
  const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
35
44
  const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
@@ -194,6 +203,7 @@ function printJsonOrText(payload, lines, asJson) {
194
203
  }
195
204
 
196
205
  const MISSION_RUN_VALUE_FLAGS = [
206
+ '--slots',
197
207
  '--max-ticks',
198
208
  '--max-wall',
199
209
  '--minutes',
@@ -213,6 +223,8 @@ const MISSION_RUN_VALUE_FLAGS = [
213
223
  const MISSION_RUN_BOOLEAN_FLAGS = [
214
224
  '--json',
215
225
  '--due',
226
+ '--fleet',
227
+ '--dry-run',
216
228
  '--no-claude',
217
229
  '--no-verify',
218
230
  '--complete-on-pass',
@@ -227,6 +239,7 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
227
239
  '--no-room-auto-run',
228
240
  '--allow-native-goal-supersede',
229
241
  '--supersede-paused-native-goal',
242
+ '--take-goal-slot',
230
243
  '--always-on',
231
244
  '--xp-task',
232
245
  '--agent-xp',
@@ -1619,7 +1632,7 @@ function missionFromArgs(args) {
1619
1632
  '--native-goal-objective',
1620
1633
  '--visible-goal-status',
1621
1634
  '--visible-goal-objective',
1622
- ], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--allow-native-goal-supersede', '--supersede-paused-native-goal']).join(' ').trim();
1635
+ ], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--allow-native-goal-supersede', '--supersede-paused-native-goal', '--take-goal-slot']).join(' ').trim();
1623
1636
  if (!objective) {
1624
1637
  exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
1625
1638
  }
@@ -2795,6 +2808,9 @@ function startMission(args) {
2795
2808
  const warnings = [missingVerifierWarning(mission)].filter(Boolean);
2796
2809
  ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
2797
2810
  const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective });
2811
+ const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
2812
+ ? takeCodexGoalSlotForMission(saved, process.cwd())
2813
+ : null;
2798
2814
  const memberState = renderMemberMissionState(saved.owner);
2799
2815
  const logPath = appendMemberLog(saved.owner, 'Mission started', {
2800
2816
  mission: saved.objective,
@@ -2805,7 +2821,7 @@ function startMission(args) {
2805
2821
  });
2806
2822
  const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
2807
2823
  printJsonOrText(
2808
- { ok: true, action: 'mission_started', mission: saved, warnings, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath, worktree_baseline: worktreeBaseline ? { path: path.relative(process.cwd(), missionBaselinePath(saved.id)), dirty_count: worktreeBaseline.dirty_count, dirty_hash: worktreeBaseline.dirty_hash } : null },
2824
+ { ok: true, action: 'mission_started', mission: saved, warnings, goal_slot_handoff: goalSlotHandoff, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath, worktree_baseline: worktreeBaseline ? { path: path.relative(process.cwd(), missionBaselinePath(saved.id)), dirty_count: worktreeBaseline.dirty_count, dirty_hash: worktreeBaseline.dirty_hash } : null },
2809
2825
  [
2810
2826
  `Started mission: ${saved.objective}`,
2811
2827
  `Owner: ${saved.owner}`,
@@ -2920,6 +2936,9 @@ function startMissionFromRunObjective(objective, args) {
2920
2936
  ...(nativeGoalObjective ? { nativeGoalObjective } : {}),
2921
2937
  ...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
2922
2938
  };
2939
+ const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
2940
+ ? takeCodexGoalSlotForMission(saved, process.cwd(), nativeGoalOptions)
2941
+ : null;
2923
2942
  const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: saved.id });
2924
2943
  const codexGoalState = runnerUsesCallerSession(saved.runner)
2925
2944
  ? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
@@ -2944,6 +2963,7 @@ function startMissionFromRunObjective(objective, args) {
2944
2963
  } : null,
2945
2964
  completed_continuation_goal: completedContinuationGoal,
2946
2965
  direct_goal_request: directGoalRequest,
2966
+ goal_slot_handoff: goalSlotHandoff,
2947
2967
  atris_goal_state: atrisGoalState,
2948
2968
  codex_goal_state: codexGoalState,
2949
2969
  requires_native_goal_start: codexGoalState?.requires_native_goal_start === true || codexGoalState?.goal?.requires_native_goal_start === true,
@@ -4611,11 +4631,11 @@ function codexGoalObjective(mission) {
4611
4631
  return mission.objective;
4612
4632
  }
4613
4633
 
4614
- function codexNativeGoalAck(mission, objective = codexGoalObjective(mission)) {
4634
+ function codexNativeGoalAck(mission) {
4615
4635
  const ack = mission?.native_goal_ack || null;
4616
4636
  if (!ack || String(ack.runtime || '').toLowerCase() !== 'codex') return null;
4617
4637
  if (ack.status !== 'active') return null;
4618
- if (String(ack.objective || '') !== String(objective || '')) return null;
4638
+ if (ack.mission_id && String(ack.mission_id) !== String(mission?.id || '')) return null;
4619
4639
  return ack;
4620
4640
  }
4621
4641
 
@@ -4821,6 +4841,33 @@ function activeCodexVisibleGoalOwner(root = process.cwd(), excludeId = '', now =
4821
4841
  return candidates[0] || null;
4822
4842
  }
4823
4843
 
4844
+ function pauseMissionRecord(mission, reason, root = process.cwd()) {
4845
+ const next = {
4846
+ ...mission,
4847
+ status: 'paused',
4848
+ paused_at: stampIso(),
4849
+ stop_reason: reason,
4850
+ next_action: `resume with: atris mission tick ${mission.id}`,
4851
+ };
4852
+ const { mission: saved } = saveMission(next, root, 'mission_paused', { reason, source: 'goal_slot_handoff' });
4853
+ appendMemberLog(saved.owner, 'Mission paused', { mission: saved.objective, reason });
4854
+ return saved;
4855
+ }
4856
+
4857
+ function takeCodexGoalSlotForMission(newMission, root = process.cwd(), options = {}) {
4858
+ if (!newMission || !isCodexGoalMission(newMission)) return null;
4859
+ const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
4860
+ const activeOwner = activeCodexVisibleGoalOwner(root, newMission.id, new Date(), runtimeGoalState);
4861
+ if (!activeOwner) return null;
4862
+ const reason = `visible goal replaced by ${newMission.id}`;
4863
+ const paused = pauseMissionRecord(activeOwner, reason, root);
4864
+ return {
4865
+ paused_mission_id: paused.id,
4866
+ paused_mission: missionStatusView(paused),
4867
+ reason,
4868
+ };
4869
+ }
4870
+
4824
4871
  function codexGoalActiveConflictPayload(newMission, activeMission, request = null, heartbeatMode = false, runtimeGoalState = null) {
4825
4872
  const newObjective = codexGoalObjective(newMission);
4826
4873
  const pausedConflict = runtimeGoalState?.status === 'paused';
@@ -5119,7 +5166,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
5119
5166
  const taskSpine = missionTaskSpine(mission);
5120
5167
  const missionView = missionStatusView(mission);
5121
5168
  const objective = codexGoalObjective(mission);
5122
- const ack = codexNativeGoalAck(mission, objective);
5169
+ const ack = codexNativeGoalAck(mission);
5123
5170
  const runtimeNeedsReplace = !ack && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
5124
5171
  const nativeGoalAction = ack
5125
5172
  ? null
@@ -5777,6 +5824,21 @@ async function runMission(args) {
5777
5824
  help();
5778
5825
  return;
5779
5826
  }
5827
+ // --fleet: staff every idle capable engine on the board's claimable
5828
+ // safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
5829
+ // full loop — see lib/fleet.js. --dry-run previews the staffing only.
5830
+ if (hasFlag(args, '--fleet')) {
5831
+ const { runFleetFlight } = require('../lib/fleet');
5832
+ const slots = Math.max(1, Number(readFlag(args, '--slots', '')) || 3);
5833
+ const flight = await runFleetFlight({
5834
+ slots,
5835
+ dryRun: hasFlag(args, '--dry-run'),
5836
+ log: asJson ? () => {} : console.log,
5837
+ });
5838
+ if (asJson) console.log(JSON.stringify(flight, null, 2));
5839
+ process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
5840
+ return;
5841
+ }
5780
5842
  const dueMode = hasFlag(args, '--due');
5781
5843
  const skipClaude = hasFlag(args, '--no-claude');
5782
5844
  const verifyEach = !hasFlag(args, '--no-verify');
@@ -6723,16 +6785,15 @@ function ackMissionGoal(args) {
6723
6785
  releaseMissionLock(lock);
6724
6786
  exitMissionError(`Mission "${ref}" uses runner=${mission.runner || 'manual'}; native Codex goal ack is only for runner=codex_goal.`, 2, asJson);
6725
6787
  }
6726
- const expectedObjective = codexGoalObjective(mission);
6727
- const objective = readFlag(args, '--objective', expectedObjective);
6728
- if (objective !== expectedObjective) {
6729
- releaseMissionLock(lock);
6730
- exitMissionError(`Native goal objective mismatch. Expected: ${expectedObjective}`, 2, asJson);
6731
- }
6788
+ const canonicalObjective = codexGoalObjective(mission);
6789
+ const reportedObjective = readFlag(args, '--objective', canonicalObjective);
6790
+ const objective = canonicalObjective;
6732
6791
  const ack = {
6733
6792
  runtime: 'codex',
6734
6793
  status: 'active',
6794
+ mission_id: mission.id,
6735
6795
  objective,
6796
+ ...(reportedObjective && reportedObjective !== objective ? { reported_objective: reportedObjective } : {}),
6736
6797
  acknowledged_at: stampIso(),
6737
6798
  };
6738
6799
  const nextMission = {
@@ -6848,13 +6909,15 @@ function help() {
6848
6909
  console.log(`
6849
6910
  atris mission - durable goal + loop + owner + proof state
6850
6911
 
6851
- atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree]
6912
+ atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree] [--take-goal-slot]
6852
6913
  [--runner manual|claude|atris2|codex_goal] [--model <id>]
6853
6914
  (runner claude spawns local claude -p per tick, --model passes through;
6854
6915
  runner atris2 runs each tick as one /atris2/turn on the AtrisOS backend,
6855
6916
  default model atris:fast; runner codex_goal publishes the goal for a live
6856
6917
  Codex session to pull via atris mission goal)
6857
6918
  atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
6919
+ atris mission inspect <id> --fields status,runner,ack,pings [--json]
6920
+ Field-selectable mission state (status, runner, native goal ack, ping counts)
6858
6921
  atris mission doctor [--local] [--json] Flag no-verifier missions, help missions, stale ready receipts, and blocked always-on loops
6859
6922
  atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
6860
6923
  atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
@@ -6869,8 +6932,9 @@ atris mission - durable goal + loop + owner + proof state
6869
6932
  atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
6870
6933
  atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
6871
6934
  atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
6935
+ atris mission run --fleet [--slots 3] [--dry-run] [--json] Staff every idle capable engine on claimable safe-lane tasks: parallel worktree builds, serial rebase-before-ship landings, receipt in atris/runs/
6872
6936
  atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
6873
- [--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede]
6937
+ [--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--take-goal-slot]
6874
6938
  [--spend-full-budget|--use-whole-budget|--stop-when-done] [--room-preflight|--no-room-preflight]
6875
6939
  [--room-auto-run|--no-room-auto-run]
6876
6940
  [--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
@@ -7149,7 +7213,39 @@ function findMissionAcrossWorktrees(ref, root = process.cwd()) {
7149
7213
  // atris mission ping <id> "<message>" — leave a note the mission's next tick
7150
7214
  // reads (and consumes) as operator direction. This is how you talk to an
7151
7215
  // always-on member mid-run without stopping it.
7152
- function pingMission(args) {
7216
+ function inspectMission(args) {
7217
+ const asJson = wantsJson(args);
7218
+ const parsed = readFieldsFlag(args, '--fields');
7219
+ if (!parsed || parsed.error) {
7220
+ exitMissionError(
7221
+ parsed?.error || 'Usage: atris mission inspect <id> --fields status,runner,ack,pings [--json]',
7222
+ 2,
7223
+ asJson,
7224
+ );
7225
+ }
7226
+ const fieldError = validateFields(parsed.fields, MISSION_INSPECT_FIELDS, 'mission');
7227
+ if (fieldError) exitMissionError(fieldError, 2, asJson);
7228
+ const ref = stripInspectArgs(args)[0] || '';
7229
+ if (!ref) {
7230
+ exitMissionError('Usage: atris mission inspect <id> --fields status,runner,ack,pings [--json]', 2, asJson);
7231
+ }
7232
+ const found = findMissionAcrossWorktrees(ref);
7233
+ if (!found) exitMissingMission(ref, 1, asJson);
7234
+ const { mission } = found;
7235
+ const values = missionInspectFieldValues(mission, parsed.fields);
7236
+ const payload = buildInspectPayload({
7237
+ action: 'mission_inspect',
7238
+ idKey: 'mission_id',
7239
+ idValue: mission.id,
7240
+ fields: parsed.fields,
7241
+ values,
7242
+ });
7243
+ printJsonOrText(payload, inspectTextLines(parsed.fields, values), asJson);
7244
+ }
7245
+
7246
+ // always-on member mid-run without stopping it. opts.silent skips console
7247
+ // output so callers (member ping) can compose their own multi-lane report.
7248
+ function pingMission(args, opts = {}) {
7153
7249
  const asJson = args.includes('--json');
7154
7250
  const rest = args.filter((a) => a !== '--json');
7155
7251
  let from = process.env.USER || 'operator';
@@ -7181,10 +7277,12 @@ function pingMission(args) {
7181
7277
  { from, text: text.slice(0, 200) },
7182
7278
  ).mission;
7183
7279
  const pending = (saved.pings || []).filter((p) => p && !p.consumed_at).length;
7184
- if (asJson) {
7185
- console.log(JSON.stringify({ ok: true, action: 'mission_ping', mission_id: saved.id, pending_pings: pending, ping }));
7186
- } else {
7187
- console.log(`pinged ${saved.id} the next tick reads it (${pending} unread).`);
7280
+ if (!opts.silent) {
7281
+ if (asJson) {
7282
+ console.log(JSON.stringify({ ok: true, action: 'mission_ping', mission_id: saved.id, pending_pings: pending, ping }));
7283
+ } else {
7284
+ console.log(`pinged ${saved.id} — the next tick reads it (${pending} unread).`);
7285
+ }
7188
7286
  }
7189
7287
  return saved;
7190
7288
  }
@@ -7235,6 +7333,8 @@ function missionCommand(args) {
7235
7333
  return goalLoopMission(rest);
7236
7334
  case 'ping':
7237
7335
  return pingMission(rest);
7336
+ case 'inspect':
7337
+ return inspectMission(rest);
7238
7338
  case 'tick':
7239
7339
  return tickMission(rest);
7240
7340
  case 'run':
@@ -7257,6 +7357,7 @@ function missionCommand(args) {
7257
7357
 
7258
7358
  module.exports = {
7259
7359
  missionCommand,
7360
+ inspectMission,
7260
7361
  missionHeartbeatLines,
7261
7362
  listMissions,
7262
7363
  listWorktreeRollupMissions,