atris 3.30.1 → 3.30.2

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 (67) hide show
  1. package/AGENTS.md +6 -0
  2. package/atris/AGENTS.md +11 -0
  3. package/atris/CLAUDE.md +5 -0
  4. package/atris/atris.md +19 -4
  5. package/atris/policies/atris-design.md +71 -0
  6. package/atris/policies/design-seed.md +187 -0
  7. package/atris/skills/atris/SKILL.md +26 -3
  8. package/atris/skills/design/SKILL.md +3 -2
  9. package/atris/skills/loop/SKILL.md +5 -3
  10. package/atris/team/_template/MEMBER.md +19 -0
  11. package/atris.md +63 -23
  12. package/ax +1434 -1698
  13. package/bin/atris.js +5 -1
  14. package/commands/agent-spawn.js +2 -2
  15. package/commands/brain.js +92 -7
  16. package/commands/brainstorm.js +62 -22
  17. package/commands/business-sync.js +92 -8
  18. package/commands/business.js +13 -7
  19. package/commands/chat-scan.js +102 -0
  20. package/commands/computer.js +758 -15
  21. package/commands/deck.js +505 -105
  22. package/commands/init.js +6 -1
  23. package/commands/launchpad.js +638 -0
  24. package/commands/log-sync.js +44 -13
  25. package/commands/loop-front.js +290 -0
  26. package/commands/member.js +124 -3
  27. package/commands/mission.js +653 -30
  28. package/commands/pull.js +37 -28
  29. package/commands/pulse.js +11 -8
  30. package/commands/run.js +79 -39
  31. package/commands/sync.js +36 -17
  32. package/commands/task.js +342 -66
  33. package/commands/xp.js +3 -0
  34. package/commands/youtube.js +221 -7
  35. package/decks/README.md +89 -0
  36. package/decks/archetype-catalog.json +180 -0
  37. package/decks/atris-antislop-pitch.json +48 -0
  38. package/decks/atris-archetypes-v2.json +83 -0
  39. package/decks/atris-one-loop-pitch.json +80 -0
  40. package/decks/atris-seed-pitch-v3.json +118 -0
  41. package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
  42. package/decks/atris-seed-pitch-v5.json +109 -0
  43. package/decks/atris-seed-pitch-v6.json +137 -0
  44. package/decks/atris-seed-pitch-v7.json +133 -0
  45. package/decks/atris-single-shot-proof.json +74 -0
  46. package/decks/mark-pincus-narrative.json +102 -0
  47. package/decks/mark-pincus-sourcery.json +94 -0
  48. package/decks/yash-applied-compute-detailed.json +150 -0
  49. package/decks/yash-applied-compute-generalist.json +82 -0
  50. package/decks/yash-applied-compute-narrative.json +54 -0
  51. package/lib/ax-prefs.js +5 -12
  52. package/lib/chat-log-scan.js +377 -0
  53. package/lib/context-gatherer.js +35 -1
  54. package/lib/deck-compose.js +145 -0
  55. package/lib/deck-history.js +64 -0
  56. package/lib/deck-layout.js +169 -0
  57. package/lib/deck-review.js +431 -0
  58. package/lib/deck-schema.js +154 -0
  59. package/lib/file-ops.js +2 -2
  60. package/lib/functional-owner.js +189 -0
  61. package/lib/slides-deck.js +512 -58
  62. package/lib/task-db.js +109 -2
  63. package/package.json +2 -1
  64. package/templates/business-starter/team/START_HERE.md +12 -8
  65. package/utils/auth.js +4 -0
  66. package/utils/config.js +4 -0
  67. package/atris/atrisDev.md +0 -717
@@ -8,6 +8,10 @@ const {
8
8
  resolveClaudeRunnerModel,
9
9
  resolveClaudeRunnerBin,
10
10
  } = require('../lib/runner-command');
11
+ const {
12
+ normalizeOwnerSlug,
13
+ resolveFunctionalOwner,
14
+ } = require('../lib/functional-owner');
11
15
 
12
16
  const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
13
17
  const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
@@ -166,27 +170,33 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
166
170
  const db = taskDb.open();
167
171
  const workspaceRoot = taskDb.workspaceRoot(root);
168
172
  const title = `Mission XP: ${mission.objective}`;
173
+ const ownerResolution = resolveMissionOwner(mission, workspaceRoot);
174
+ const owner = ownerResolution.owner;
169
175
  const metadata = {
170
- assigned_to: mission.owner,
176
+ assigned_to: owner,
171
177
  delegate_via: 'mission_goal_loop',
172
178
  created_for_day: todayName(),
173
179
  goal_id: mission.id,
174
180
  goal_objective: mission.objective,
175
181
  mission_id: mission.id,
176
182
  mission_objective: mission.objective,
177
- mission_owner: mission.owner,
183
+ mission_owner: owner,
184
+ owner_resolution: ownerResolution.reason,
178
185
  mission_lane: mission.lane,
179
186
  mission_runner: mission.runner,
180
187
  verify: mission.verifier || null,
181
188
  stop_condition: mission.stop_condition || null,
182
189
  };
190
+ if (ownerResolution.requested_owner && ownerResolution.requested_owner !== owner) metadata.requested_owner = ownerResolution.requested_owner;
191
+ if (ownerResolution.executed_by) metadata.executed_by = normalizeOwnerSlug(ownerResolution.executed_by);
192
+ if (ownerResolution.proposed_member) metadata.proposed_member = ownerResolution.proposed_member;
183
193
  const result = taskDb.addTask(db, {
184
194
  title,
185
195
  tag: 'agent-xp',
186
196
  workspaceRoot,
187
197
  sourceKey: `mission-xp:${mission.id}`,
188
198
  status: 'claimed',
189
- claimedBy: mission.owner,
199
+ claimedBy: owner,
190
200
  metadata,
191
201
  });
192
202
  const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
@@ -194,7 +204,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
194
204
  if (task) {
195
205
  taskDb.noteTask(db, {
196
206
  id: task.id,
197
- actor: process.env.ATRIS_AGENT_ID || mission.owner || 'mission-lead',
207
+ actor: process.env.ATRIS_AGENT_ID || owner,
198
208
  content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP lands only after human accept.`,
199
209
  });
200
210
  }
@@ -204,7 +214,9 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
204
214
  ref: missionTaskRef(task) || result.id,
205
215
  title,
206
216
  status: task?.status || 'claimed',
207
- assigned_to: mission.owner,
217
+ assigned_to: owner,
218
+ owner_resolution: ownerResolution.reason,
219
+ executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
208
220
  inserted: result.inserted !== false,
209
221
  projection_path: outPath,
210
222
  };
@@ -223,6 +235,24 @@ function statePaths(root = process.cwd()) {
223
235
  };
224
236
  }
225
237
 
238
+ function readBusinessBinding(root = process.cwd()) {
239
+ const file = path.join(root, '.atris', 'business.json');
240
+ try {
241
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
242
+ return {
243
+ business_id: parsed.business_id || '',
244
+ workspace_id: parsed.workspace_id || '',
245
+ slug: parsed.slug || '',
246
+ };
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ function businessIdForAtris2Mission(mission, cwd = process.cwd()) {
253
+ return mission?.business_id || readBusinessBinding(cwd)?.business_id || null;
254
+ }
255
+
226
256
  function readJsonLines(file) {
227
257
  if (!fs.existsSync(file)) return [];
228
258
  return fs.readFileSync(file, 'utf8')
@@ -424,6 +454,7 @@ function renderMemberNowMarkdown(owner, missions) {
424
454
  return lines.join('\n');
425
455
  }
426
456
  for (const mission of missions) {
457
+ const taskSpine = missionTaskSpine(mission);
427
458
  lines.push(`## ${mission.objective}`);
428
459
  lines.push('');
429
460
  lines.push(`- id: ${mission.id}`);
@@ -431,6 +462,9 @@ function renderMemberNowMarkdown(owner, missions) {
431
462
  lines.push(`- cadence: ${mission.cadence}`);
432
463
  lines.push(`- runner: ${mission.runner}${mission.model ? ` (${mission.model})` : ''}`);
433
464
  lines.push(`- lane: ${mission.lane}`);
465
+ if (taskSpine.task_ref) lines.push(`- task: ${taskSpine.task_ref}`);
466
+ if (taskSpine.current_step_command) lines.push(`- task next: ${taskSpine.current_step_command}`);
467
+ if (!taskSpine.has_task && taskSpine.ensure_task_command) lines.push(`- task setup: ${taskSpine.ensure_task_command}`);
434
468
  if (mission.xp_task?.ref) lines.push(`- AgentXP task: ${mission.xp_task.ref}`);
435
469
  if (mission.verifier) lines.push(`- verifier: ${mission.verifier}`);
436
470
  if (mission.stop_condition) lines.push(`- stop: ${mission.stop_condition}`);
@@ -463,6 +497,52 @@ function completionGateLabel(gate) {
463
497
  return gate.forced ? `forced override (${gate.source})` : gate.source;
464
498
  }
465
499
 
500
+ function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
501
+ const gate = mission.completion_gate || {};
502
+ const gateLabel = completionGateLabel(gate) || 'completion gate';
503
+ const checked = gate.source === 'receipt' && gate.receipt_path
504
+ ? `I checked the passing verifier receipt ${gate.receipt_path}.`
505
+ : gate.source === 'mission_state'
506
+ ? 'I checked mission state showing the verifier passed.'
507
+ : gate.source === 'no_verifier'
508
+ ? 'No verifier was configured, so completion used the no-verifier gate.'
509
+ : `I checked the ${gateLabel} completion gate.`;
510
+ const tested = mission.verifier_result?.passed
511
+ ? `Verifier passed: ${mission.verifier_result.command || mission.verifier || 'configured verifier'}.`
512
+ : mission.verifier
513
+ ? `Completion proof is attached for verifier: ${mission.verifier}.`
514
+ : 'No verifier command was recorded for this mission.';
515
+ const landing = {
516
+ happened: `Mission completed: ${mission.objective}.`,
517
+ checked,
518
+ tested,
519
+ saved: `Saved complete mission ${mission.id}${proof ? ` with proof ${proof}` : ''}.`,
520
+ decision: xpNextCommand
521
+ ? `Queue AgentXP next: ${xpNextCommand}`
522
+ : 'Mission is complete; start or resume the next mission if more work remains.',
523
+ };
524
+ const result = {
525
+ changed: landing.happened,
526
+ checked: landing.checked,
527
+ tested: landing.tested,
528
+ saved: landing.saved,
529
+ accept: landing.decision,
530
+ };
531
+ return { landing, result };
532
+ }
533
+
534
+ function missionResultLines(completion) {
535
+ const landing = completion?.landing || {};
536
+ const result = completion?.result || {};
537
+ const lines = ['Result:'];
538
+ if (landing.happened) lines.push(` What happened: ${landing.happened}`);
539
+ if (landing.checked) lines.push(` How I checked: ${landing.checked}`);
540
+ if (landing.tested) lines.push(` What I tested: ${landing.tested}`);
541
+ if (result.saved) lines.push(` Saved: ${result.saved}`);
542
+ if (landing.decision) lines.push(` Decision: ${landing.decision}`);
543
+ return lines;
544
+ }
545
+
466
546
  function renderMissionStatus(root = process.cwd()) {
467
547
  const paths = statePaths(root);
468
548
  const missions = listMissions(root);
@@ -478,10 +558,14 @@ function renderMissionStatus(root = process.cwd()) {
478
558
  lines.push('No missions yet.', '');
479
559
  } else {
480
560
  for (const mission of missions.slice(0, 12)) {
561
+ const taskSpine = missionTaskSpine(mission);
481
562
  lines.push(`- **${mission.id}** ${mission.objective}`);
482
563
  lines.push(` - owner: ${mission.owner}`);
483
564
  lines.push(` - state: ${mission.status}`);
484
565
  lines.push(` - next: ${mission.next_action || 'tick or verify'}`);
566
+ if (taskSpine?.task_ref) lines.push(` - task: ${taskSpine.task_ref}`);
567
+ if (taskSpine?.current_step_command) lines.push(` - task next: ${taskSpine.current_step_command}`);
568
+ if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) lines.push(` - task setup: ${taskSpine.ensure_task_command}`);
485
569
  if (mission.xp_task?.ref) lines.push(` - AgentXP task: ${mission.xp_task.ref}`);
486
570
  if (mission.receipt_path) lines.push(` - proof: ${mission.receipt_path}`);
487
571
  const gateLabel = completionGateLabel(mission.completion_gate);
@@ -501,13 +585,110 @@ function missionXpTaskRefFromMission(mission) {
501
585
  return '';
502
586
  }
503
587
 
588
+ function resolveMissionOwner(mission, root = process.cwd()) {
589
+ const requestedOwner = mission?.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
590
+ const resolved = resolveFunctionalOwner({
591
+ requestedOwner,
592
+ title: mission?.objective || '',
593
+ tag: mission?.lane || 'mission',
594
+ note: mission?.next_action || '',
595
+ goal: mission?.objective || '',
596
+ root,
597
+ fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
598
+ });
599
+ const requested = mission?.requested_owner || (
600
+ resolved.requested_owner && resolved.requested_owner !== resolved.owner ? resolved.requested_owner : null
601
+ );
602
+ return {
603
+ ...resolved,
604
+ reason: mission?.owner_resolution || resolved.reason,
605
+ requested_owner: requested,
606
+ executed_by: mission?.executed_by || resolved.executed_by || null,
607
+ };
608
+ }
609
+
610
+ function applyMissionOwnerResolution(mission, root = process.cwd()) {
611
+ const ownerResolution = resolveMissionOwner(mission, root);
612
+ const next = {
613
+ ...mission,
614
+ owner: ownerResolution.owner,
615
+ owner_resolution: ownerResolution.reason,
616
+ };
617
+ if (ownerResolution.requested_owner && ownerResolution.requested_owner !== ownerResolution.owner) {
618
+ next.requested_owner = ownerResolution.requested_owner;
619
+ }
620
+ if (ownerResolution.executed_by) {
621
+ next.executed_by = normalizeOwnerSlug(ownerResolution.executed_by);
622
+ }
623
+ if (ownerResolution.proposed_member) {
624
+ next.proposed_member = ownerResolution.proposed_member;
625
+ }
626
+ return { mission: next, ownerResolution };
627
+ }
628
+
504
629
  function missionXpReadyAction(mission, receiptPath) {
505
630
  const ref = missionXpTaskRefFromMission(mission);
506
631
  if (!ref || !receiptPath) return null;
507
- const owner = mission.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
632
+ const owner = resolveMissionOwner(mission).owner;
508
633
  return `queue AgentXP review: atris task current-step --goal-id ${mission.id} --as ${owner} --proof "${receiptPath}" --json`;
509
634
  }
510
635
 
636
+ function missionTaskSpine(mission) {
637
+ if (!mission || !mission.id) return null;
638
+ const ownerResolution = resolveMissionOwner(mission);
639
+ const taskIds = Array.isArray(mission.task_ids) ? mission.task_ids.filter(Boolean) : [];
640
+ const taskId = mission.xp_task?.task_id
641
+ || mission.current_task_id
642
+ || mission.task_id
643
+ || taskIds[0]
644
+ || null;
645
+ const taskRef = mission.xp_task?.ref
646
+ || mission.task_ref
647
+ || (taskId ? String(taskId) : null);
648
+ const owner = ownerResolution.owner;
649
+ return {
650
+ schema: 'atris.mission_task_spine.v1',
651
+ goal_id: mission.id,
652
+ owner,
653
+ requested_owner: ownerResolution.requested_owner || null,
654
+ owner_resolution: ownerResolution.reason,
655
+ executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
656
+ lane: mission.lane || 'workspace',
657
+ runner: mission.runner || 'manual',
658
+ task_id: taskId,
659
+ current_task_id: taskId,
660
+ task_ref: taskRef,
661
+ has_task: Boolean(taskId || taskRef),
662
+ current_step_command: taskId || taskRef
663
+ ? `atris task current-step --goal-id ${mission.id} --as ${owner} --proof "<proof>" --json`
664
+ : null,
665
+ ensure_task_command: taskId || taskRef
666
+ ? null
667
+ : `atris mission attach-task ${mission.id} --json`,
668
+ };
669
+ }
670
+
671
+ function missionStatusView(mission) {
672
+ const taskSpine = missionTaskSpine(mission);
673
+ if (!taskSpine) return mission;
674
+ const requestedOwner = taskSpine.requested_owner
675
+ || mission.requested_owner
676
+ || (mission.owner && mission.owner !== taskSpine.owner ? mission.owner : null);
677
+ return {
678
+ ...mission,
679
+ owner: taskSpine.owner,
680
+ functional_owner: taskSpine.owner,
681
+ requested_owner: requestedOwner,
682
+ owner_resolution: taskSpine.owner_resolution,
683
+ executed_by: taskSpine.executed_by || mission.executed_by || null,
684
+ goal_id: taskSpine.goal_id,
685
+ task_id: taskSpine.task_id,
686
+ current_task_id: taskSpine.current_task_id,
687
+ task_ref: taskSpine.task_ref,
688
+ task_spine: taskSpine,
689
+ };
690
+ }
691
+
511
692
  function missionFromArgs(args) {
512
693
  const objective = stripKnownFlags(args, [
513
694
  '--owner',
@@ -524,11 +705,20 @@ function missionFromArgs(args) {
524
705
  if (!objective) {
525
706
  exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
526
707
  }
527
- const owner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
708
+ const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
528
709
  const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
529
710
  const runner = readFlag(args, '--runner', 'manual');
530
711
  const model = readFlag(args, '--model', '') || (String(runner).toLowerCase() === 'atris2' ? 'atris:fast' : '');
531
712
  const lane = readFlag(args, '--lane', 'workspace');
713
+ const ownerResolution = resolveFunctionalOwner({
714
+ requestedOwner,
715
+ title: objective,
716
+ tag: lane,
717
+ goal: objective,
718
+ root: process.cwd(),
719
+ fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
720
+ });
721
+ const owner = ownerResolution.owner;
532
722
  const verifier = readFlag(args, '--verify', '');
533
723
  assertMissionVerifier(verifier, wantsJson(args));
534
724
  const stopCondition = readFlag(args, '--stop', verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof');
@@ -536,6 +726,7 @@ function missionFromArgs(args) {
536
726
  const humanAsks = readRepeatedFlag(args, '--ask');
537
727
  const alwaysOn = hasFlag(args, '--always-on');
538
728
  const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
729
+ const businessBinding = readBusinessBinding(process.cwd());
539
730
  const id = missionId(objective);
540
731
  const mission = {
541
732
  schema: 'atris.mission.v1',
@@ -543,10 +734,16 @@ function missionFromArgs(args) {
543
734
  slug: slugify(objective),
544
735
  objective,
545
736
  owner,
737
+ owner_resolution: ownerResolution.reason,
738
+ ...(ownerResolution.requested_owner && ownerResolution.requested_owner !== owner ? { requested_owner: ownerResolution.requested_owner } : {}),
739
+ ...(ownerResolution.executed_by ? { executed_by: normalizeOwnerSlug(ownerResolution.executed_by) } : {}),
740
+ ...(ownerResolution.proposed_member ? { proposed_member: ownerResolution.proposed_member } : {}),
546
741
  status: 'planning',
547
742
  cadence,
548
743
  runner,
549
744
  ...(model ? { model } : {}),
745
+ ...(businessBinding?.business_id ? { business_id: businessBinding.business_id } : {}),
746
+ ...(businessBinding?.workspace_id ? { workspace_id: businessBinding.workspace_id } : {}),
550
747
  lane,
551
748
  verifier,
552
749
  always_on: alwaysOn,
@@ -571,6 +768,36 @@ function missingVerifierWarning(mission) {
571
768
  };
572
769
  }
573
770
 
771
+ function missionRunSmokeVerifier() {
772
+ const script = [
773
+ "const fs=require('fs')",
774
+ "const os=require('os')",
775
+ "const path=require('path')",
776
+ "const {spawnSync}=require('child_process')",
777
+ "const root=fs.mkdtempSync(path.join(os.tmpdir(),'atris-mission-run-verifier-'))",
778
+ "process.on('exit',()=>fs.rmSync(root,{recursive:true,force:true}))",
779
+ "fs.mkdirSync(path.join(root,'atris'),{recursive:true})",
780
+ "const r=spawnSync('atris',['mission','run','verifier smoke objective','--json'],{cwd:root,encoding:'utf8',env:{...process.env,ATRIS_SKIP_UPDATE_CHECK:'1'}})",
781
+ "if(r.status!==0){process.stderr.write(r.stderr||r.stdout);process.exit(1)}",
782
+ "const p=JSON.parse(r.stdout)",
783
+ "if(p.action!=='mission_run_started')process.exit(2)",
784
+ "if(p.mission?.runner!=='codex_goal')process.exit(3)",
785
+ "if(p.codex_goal_state?.goal?.visible_goal?.schema!=='atris.visible_chat_goal_bridge.v1')process.exit(4)",
786
+ ].join(';');
787
+ return `node -e ${JSON.stringify(script)}`;
788
+ }
789
+
790
+ function inferRunObjectiveVerifier(objective, root = process.cwd()) {
791
+ const text = String(objective || '').toLowerCase();
792
+ if (!/\bmission\s+run\b/.test(text)) return '';
793
+ const sourceTest = path.join(root, 'test', 'mission-status.test.js');
794
+ const sourceCommand = path.join(root, 'commands', 'mission.js');
795
+ if (fs.existsSync(sourceTest) && fs.existsSync(sourceCommand)) {
796
+ return 'node --test test/mission-status.test.js';
797
+ }
798
+ return missionRunSmokeVerifier();
799
+ }
800
+
574
801
  function startMission(args) {
575
802
  const asJson = wantsJson(args);
576
803
  const mission = missionFromArgs(args);
@@ -624,6 +851,137 @@ function startMission(args) {
624
851
  );
625
852
  }
626
853
 
854
+ function startMissionFromRunObjective(objective, args) {
855
+ const asJson = wantsJson(args);
856
+ const verifier = readFlag(args, '--verify', inferRunObjectiveVerifier(objective));
857
+ const stopCondition = readFlag(
858
+ args,
859
+ '--stop',
860
+ verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready',
861
+ );
862
+ const startArgs = [
863
+ objective,
864
+ '--owner',
865
+ readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead'),
866
+ '--runner',
867
+ readFlag(args, '--runner', 'codex_goal'),
868
+ '--lane',
869
+ readFlag(args, '--lane', 'workspace'),
870
+ '--cadence',
871
+ readFlag(args, '--cadence', 'manual'),
872
+ '--stop',
873
+ stopCondition,
874
+ ];
875
+ if (verifier) startArgs.push('--verify', verifier);
876
+ const model = readFlag(args, '--model', '');
877
+ if (model) startArgs.push('--model', model);
878
+ if (hasFlag(args, '--always-on')) startArgs.push('--always-on');
879
+ if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp')) startArgs.push('--xp-task');
880
+
881
+ const mission = missionFromArgs(startArgs);
882
+ const warnings = [missingVerifierWarning(mission)].filter(Boolean);
883
+ ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
884
+ const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
885
+ const memberState = renderMemberMissionState(saved.owner);
886
+ const logPath = appendMemberLog(saved.owner, 'Mission started from run', {
887
+ mission: saved.objective,
888
+ cadence: saved.cadence,
889
+ runner: saved.runner,
890
+ lane: saved.lane,
891
+ verifier: saved.verifier,
892
+ });
893
+ const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
894
+ const codexGoalState = refreshCodexGoalController(process.cwd());
895
+ printJsonOrText(
896
+ {
897
+ ok: true,
898
+ action: 'mission_run_started',
899
+ mission: saved,
900
+ warnings,
901
+ state_path: statePaths().missionsJsonl,
902
+ member_state: memberState,
903
+ log_path: logPath,
904
+ worktree_baseline: worktreeBaseline ? {
905
+ path: path.relative(process.cwd(), missionBaselinePath(saved.id)),
906
+ dirty_count: worktreeBaseline.dirty_count,
907
+ dirty_hash: worktreeBaseline.dirty_hash,
908
+ } : null,
909
+ codex_goal_state: codexGoalState,
910
+ next_command: codexGoalState.goal?.next_command || `atris mission tick ${saved.id} --verify`,
911
+ },
912
+ [
913
+ `Started mission: ${saved.objective}`,
914
+ `Owner: ${saved.owner}`,
915
+ `Runner: ${saved.runner}`,
916
+ ...(codexGoalState.goal ? [`Visible goal: ${codexGoalState.goal.objective}`] : []),
917
+ ...warnings.map((warning) => `Warning: ${warning.message}`),
918
+ ],
919
+ asJson,
920
+ );
921
+ }
922
+
923
+ function attachMissionTask(args) {
924
+ const asJson = wantsJson(args);
925
+ const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
926
+ if (!ref) {
927
+ exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
928
+ }
929
+ const mission = resolveMission(ref);
930
+ if (!mission) {
931
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
932
+ }
933
+ if (TERMINAL_STATUSES.has(mission.status)) {
934
+ exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
935
+ }
936
+
937
+ const existingSpine = missionTaskSpine(mission);
938
+ if (existingSpine?.has_task) {
939
+ const view = missionStatusView(mission);
940
+ printJsonOrText(
941
+ { ok: true, action: 'mission_task_spine_exists', mission: view, task_spine: view.task_spine },
942
+ [
943
+ `Mission task spine already exists: ${mission.objective}`,
944
+ `Task: ${view.task_spine.task_ref}`,
945
+ `Next: ${view.task_spine.current_step_command}`,
946
+ ],
947
+ asJson,
948
+ );
949
+ return;
950
+ }
951
+
952
+ const ownership = applyMissionOwnerResolution(mission, process.cwd());
953
+ const baseMission = ownership.mission;
954
+ const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
955
+ const nextMission = {
956
+ ...baseMission,
957
+ xp_task_enabled: true,
958
+ xp_task: xpTask,
959
+ task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
960
+ };
961
+ if (nextMission.status === 'ready' && nextMission.receipt_path) {
962
+ nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
963
+ } else if (!nextMission.verifier && !nextMission.always_on) {
964
+ nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
965
+ }
966
+
967
+ const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
968
+ const memberState = renderMemberMissionState(saved.owner);
969
+ const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
970
+ mission: saved.objective,
971
+ task: xpTask.ref,
972
+ });
973
+ const view = missionStatusView(saved);
974
+ printJsonOrText(
975
+ { ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
976
+ [
977
+ `Attached task spine: ${saved.objective}`,
978
+ `Task: ${xpTask.ref}`,
979
+ `Next: ${view.task_spine.current_step_command}`,
980
+ ],
981
+ asJson,
982
+ );
983
+ }
984
+
627
985
  function statusMission(args) {
628
986
  const asJson = wantsJson(args);
629
987
  const localOnly = hasFlag(args, '--local');
@@ -648,14 +1006,15 @@ function statusMission(args) {
648
1006
  if (ref && !missions.length) {
649
1007
  exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
650
1008
  }
1009
+ const missionViews = missions.map(missionStatusView);
651
1010
  // Member state renders are cwd-local writes; rolled-up missions stay read-only.
652
- for (const owner of new Set(missions.filter((mission) => !mission.worktree_root).map((mission) => mission.owner).filter(Boolean))) {
1011
+ for (const owner of new Set(missionViews.filter((mission) => !mission.worktree_root).map((mission) => mission.owner).filter(Boolean))) {
653
1012
  renderMemberMissionState(owner);
654
1013
  }
655
1014
  const payload = {
656
1015
  ok: true,
657
1016
  action: 'mission_status',
658
- missions,
1017
+ missions: missionViews,
659
1018
  state_path: statePaths().missionsJsonl,
660
1019
  events_path: statePaths().eventsJsonl,
661
1020
  status_path: renderMissionStatus(),
@@ -663,14 +1022,18 @@ function statusMission(args) {
663
1022
  printJsonOrText(
664
1023
  payload,
665
1024
  missions.length
666
- ? missions.flatMap((mission) => [
1025
+ ? missionViews.flatMap((mission) => [
667
1026
  `Mission: ${mission.objective}`,
668
1027
  ` id: ${mission.id}`,
669
1028
  ` owner: ${mission.owner}`,
1029
+ ...(mission.executed_by ? [` executed_by: ${mission.executed_by}`] : []),
670
1030
  ` state: ${mission.status}`,
671
1031
  ...missionHeartbeatLines(mission),
672
1032
  ...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
673
1033
  ` next: ${mission.next_action || 'tick or verify'}`,
1034
+ ...(mission.task_spine?.task_ref ? [` task: ${mission.task_spine.task_ref}`] : []),
1035
+ ...(mission.task_spine?.current_step_command ? [` task next: ${mission.task_spine.current_step_command}`] : []),
1036
+ ...(!mission.task_spine?.has_task && mission.task_spine?.ensure_task_command ? [` task setup: ${mission.task_spine.ensure_task_command}`] : []),
674
1037
  ...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
675
1038
  ...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
676
1039
  ])
@@ -679,6 +1042,108 @@ function statusMission(args) {
679
1042
  );
680
1043
  }
681
1044
 
1045
+ function readMissionReceipt(receiptPath, root = process.cwd()) {
1046
+ if (!receiptPath) return null;
1047
+ const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
1048
+ try {
1049
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
1050
+ } catch {
1051
+ return null;
1052
+ }
1053
+ }
1054
+
1055
+ function firstUsefulLine(text, fallback = '') {
1056
+ return String(text || '')
1057
+ .split(/\r?\n/)
1058
+ .map((line) => line.replace(/^[-*\s#]+/, '').trim())
1059
+ .filter(Boolean)
1060
+ .find((line) => !/^(receipt|summary|final|result)$/i.test(line))
1061
+ || fallback;
1062
+ }
1063
+
1064
+ function missionWorkerLabel(mission) {
1065
+ const runner = String(mission && mission.runner || 'manual').toLowerCase();
1066
+ if (runner === 'atris2') return `Remote Atris2 computer${mission.model ? ` using ${mission.model}` : ''}`;
1067
+ if (runner === 'claude') return `Claude worker${mission.model ? ` using ${mission.model}` : ''}`;
1068
+ if (runner === 'codex_goal') return 'Codex goal handoff';
1069
+ return 'Local mission tick';
1070
+ }
1071
+
1072
+ function missionWorkerSummary(mission, receipt) {
1073
+ if (mission && mission.worker_summary) return mission.worker_summary;
1074
+ const tick = receipt && receipt.result && (receipt.result.tick || (Array.isArray(receipt.result.ticks) ? receipt.result.ticks[receipt.result.ticks.length - 1] : null));
1075
+ if (!tick) return mission && mission.last_tick_reason ? `Last tick: ${mission.last_tick_reason}` : 'No worker receipt yet.';
1076
+ if (tick.atris2) {
1077
+ return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Remote worker ran and returned a response.' : 'Remote worker failed.');
1078
+ }
1079
+ if (tick.claude) {
1080
+ if (tick.claude.skipped) {
1081
+ return tick.summary || `Worker step skipped: ${tick.claude.reason || tick.reason || 'not needed'}.`;
1082
+ }
1083
+ return tick.claude.summary || firstUsefulLine(tick.claude.receipt_text, tick.claude.ok ? 'Worker ran and returned a response.' : 'Worker failed.');
1084
+ }
1085
+ if (tick.summary) return tick.summary;
1086
+ return tick.reason ? `Worker tick: ${tick.reason}` : 'Worker tick recorded.';
1087
+ }
1088
+
1089
+ function missionReportFor(mission, root = process.cwd()) {
1090
+ const verifierReceiptPath = mission.receipt_path || null;
1091
+ const receipt = readMissionReceipt(verifierReceiptPath, root);
1092
+ const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
1093
+ const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
1094
+ const operatorOutcome = mission.operator_outcome
1095
+ || (verifierPassed ? 'Verifier passed.' : mission.status === 'complete' ? 'Mission is complete.' : mission.status === 'blocked' ? 'Mission is blocked.' : 'Mission is still in progress.');
1096
+ return {
1097
+ id: mission.id,
1098
+ objective: mission.objective,
1099
+ status: mission.status,
1100
+ operator_outcome: operatorOutcome,
1101
+ worker: mission.worker || missionWorkerLabel(mission),
1102
+ worker_summary: missionWorkerSummary(mission, receipt),
1103
+ worker_receipt_path: workerReceiptPath,
1104
+ verifier_receipt_path: verifierReceiptPath,
1105
+ operator_next: mission.operator_next || mission.next_action || 'Review the mission state.',
1106
+ };
1107
+ }
1108
+
1109
+ function reportMission(args) {
1110
+ const asJson = wantsJson(args);
1111
+ const localOnly = hasFlag(args, '--local');
1112
+ const ref = stripKnownFlags(args, ['--limit'], ['--json', '--local'])[0] || '';
1113
+ const limit = readPositiveIntegerFlag(args, '--limit', ref ? 1 : 3, { json: asJson });
1114
+ let missions = ref ? [resolveMission(ref)].filter(Boolean) : listMissions();
1115
+ if (!ref && !localOnly) {
1116
+ const seen = new Set(missions.map((mission) => mission.id));
1117
+ for (const rolled of listWorktreeRollupMissions()) {
1118
+ if (seen.has(rolled.id)) continue;
1119
+ seen.add(rolled.id);
1120
+ missions.push(rolled);
1121
+ }
1122
+ missions.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
1123
+ }
1124
+ if (ref && !missions.length) {
1125
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
1126
+ }
1127
+ missions = missions.slice(0, limit);
1128
+ const reports = missions.map((mission) => missionReportFor(mission, mission.worktree_root || process.cwd()));
1129
+ printJsonOrText(
1130
+ { ok: true, action: 'mission_report', reports },
1131
+ reports.length
1132
+ ? reports.flatMap((report) => [
1133
+ `Mission: ${report.objective}`,
1134
+ ` state: ${report.status}`,
1135
+ ` What happened: ${report.operator_outcome}`,
1136
+ ` Worker: ${report.worker}`,
1137
+ ` Worker summary: ${report.worker_summary}`,
1138
+ ...(report.worker_receipt_path ? [` Worker receipt: ${report.worker_receipt_path}`] : []),
1139
+ ...(report.verifier_receipt_path ? [` Verifier receipt: ${report.verifier_receipt_path}`] : []),
1140
+ ` Next: ${report.operator_next}`,
1141
+ ])
1142
+ : ['No missions yet. Run: atris mission start "..." --owner <member>'],
1143
+ asJson,
1144
+ );
1145
+ }
1146
+
682
1147
  // `atris mission watch [id]` — read-only live heartbeat. Prints a line per tick as it
683
1148
  // lands so a human (or any terminal) can see the loop is alive without rerunning status.
684
1149
  function watchMission(args) {
@@ -1125,10 +1590,14 @@ function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
1125
1590
  }
1126
1591
 
1127
1592
  function codexGoalObjective(mission) {
1128
- return `Advance Atris mission ${mission.id}: ${mission.objective}`;
1593
+ return mission.objective;
1129
1594
  }
1130
1595
 
1131
1596
  function codexGoalNextCommand(mission) {
1597
+ const taskSpine = missionTaskSpine(mission);
1598
+ if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) {
1599
+ return taskSpine.ensure_task_command;
1600
+ }
1132
1601
  if (mission.status === 'ready') {
1133
1602
  const xpAction = missionXpReadyAction(mission, mission.receipt_path);
1134
1603
  if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
@@ -1143,14 +1612,40 @@ function codexGoalNextCommand(mission) {
1143
1612
  return `atris mission tick ${mission.id} --summary "<what changed>"`;
1144
1613
  }
1145
1614
 
1615
+ function codexVisibleGoalBridge(mission, goalObjective) {
1616
+ return {
1617
+ schema: 'atris.visible_chat_goal_bridge.v1',
1618
+ runtime: 'codex',
1619
+ source: 'atris_mission',
1620
+ mission_id: mission.id,
1621
+ desired_objective: goalObjective,
1622
+ status: 'needs_runtime_write',
1623
+ state_file: '.atris/state/codex_goal.json',
1624
+ status_file: 'atris/status/codex-goal.md',
1625
+ operations: {
1626
+ read_current_goal: 'get_goal',
1627
+ keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
1628
+ create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
1629
+ complete_after_proof: 'update_goal({ status: "complete" })',
1630
+ refresh_next_candidate: 'atris mission goal --json',
1631
+ },
1632
+ guardrails: [
1633
+ 'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
1634
+ 'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
1635
+ ],
1636
+ };
1637
+ }
1638
+
1146
1639
  function codexGoalToolContract(mission) {
1147
1640
  return {
1148
1641
  current_policy: 'keep one visible Codex /goal active for the selected Atris mission',
1149
1642
  read_current_goal: 'get_goal',
1150
1643
  complete_current_goal: 'update_goal({ status: "complete" })',
1151
1644
  select_next_goal: 'atris mission goal --json',
1152
- set_next_goal: 'replace_goal(goal.objective) or create_goal(goal.objective) after the completed goal slot is reusable',
1153
- platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow create_goal after update_goal completes the prior goal.',
1645
+ set_next_goal: 'use goal.visible_goal: create_goal({ objective: goal.objective }) when no active goal blocks the slot',
1646
+ visible_goal_bridge: 'goal.visible_goal',
1647
+ platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
1648
+ runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof -> atris mission goal --json -> create_goal({ objective: goal.objective })',
1154
1649
  blocked_without_platform_goal_write: true,
1155
1650
  mission_id: mission.id,
1156
1651
  };
@@ -1191,6 +1686,12 @@ function writeCodexGoalState(payload, root = process.cwd()) {
1191
1686
  lines.push(`- reason: ${state.goal.reason}`);
1192
1687
  lines.push(`- objective: ${state.goal.objective}`);
1193
1688
  lines.push(`- next: ${state.goal.next_command}`);
1689
+ if (state.goal.visible_goal) {
1690
+ lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
1691
+ lines.push(`- visible goal desired: ${state.goal.visible_goal.desired_objective}`);
1692
+ lines.push(`- visible goal create: ${state.goal.visible_goal.operations.create_when_empty_or_completed}`);
1693
+ lines.push(`- visible goal complete: ${state.goal.visible_goal.operations.complete_after_proof}`);
1694
+ }
1194
1695
  lines.push(`- platform write blocked: ${state.goal.codex_tool_contract.blocked_without_platform_goal_write}`);
1195
1696
  } else {
1196
1697
  lines.push('- mission: none');
@@ -1219,14 +1720,21 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1219
1720
  }
1220
1721
 
1221
1722
  const { mission, reason } = selected;
1723
+ const taskSpine = missionTaskSpine(mission);
1724
+ const missionView = missionStatusView(mission);
1725
+ const objective = codexGoalObjective(mission);
1222
1726
  const goal = {
1223
- objective: codexGoalObjective(mission),
1727
+ objective,
1224
1728
  mission_id: mission.id,
1225
1729
  mission_objective: mission.objective,
1226
1730
  mission_status: mission.status,
1731
+ owner: taskSpine?.owner || mission.owner,
1732
+ executed_by: taskSpine?.executed_by || mission.executed_by || null,
1733
+ task_spine: taskSpine,
1227
1734
  reason,
1228
1735
  next_command: codexGoalNextCommand(mission),
1229
1736
  replace_after: 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.',
1737
+ visible_goal: codexVisibleGoalBridge(mission, objective),
1230
1738
  codex_tool_contract: codexGoalToolContract(mission),
1231
1739
  };
1232
1740
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
@@ -1234,7 +1742,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1234
1742
  ok: true,
1235
1743
  action: heartbeatMode ? 'codex_goal_heartbeat' : 'codex_goal_candidate',
1236
1744
  goal,
1237
- mission,
1745
+ mission: missionView,
1238
1746
  heartbeat,
1239
1747
  };
1240
1748
  }
@@ -1249,10 +1757,8 @@ function refreshCodexGoalController(root = process.cwd(), options = {}) {
1249
1757
  };
1250
1758
  }
1251
1759
 
1252
- function runMissionRunDueOnce(root = process.cwd(), options = {}) {
1760
+ function runAtrisMissionJsonCommand(root, args, options = {}) {
1253
1761
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
1254
- const args = ['mission', 'run', '--due', '--max-ticks', '1', '--complete-on-pass', '--json'];
1255
- if (options.noClaude) args.push('--no-claude');
1256
1762
  const result = spawnSync(process.execPath, [cliPath, ...args], {
1257
1763
  cwd: root,
1258
1764
  encoding: 'utf8',
@@ -1264,15 +1770,79 @@ function runMissionRunDueOnce(root = process.cwd(), options = {}) {
1264
1770
  payload = JSON.parse(result.stdout || '{}');
1265
1771
  } catch {}
1266
1772
  return {
1773
+ action: options.action || null,
1267
1774
  command: `atris ${args.join(' ')}`,
1268
1775
  status: result.status,
1269
1776
  ok: result.status === 0,
1777
+ heavy_work: options.heavyWork === true,
1778
+ setup_work: options.setupWork === true,
1270
1779
  stdout: String(result.stdout || '').slice(-4000),
1271
1780
  stderr: String(result.stderr || '').slice(-4000),
1272
1781
  payload,
1273
1782
  };
1274
1783
  }
1275
1784
 
1785
+ function runMissionRunDueOnce(root = process.cwd(), options = {}) {
1786
+ const args = ['mission', 'run', '--due', '--max-ticks', '1', '--complete-on-pass', '--json'];
1787
+ if (options.noClaude) args.push('--no-claude');
1788
+ return runAtrisMissionJsonCommand(root, args, {
1789
+ action: 'mission_run_due',
1790
+ heavyWork: true,
1791
+ });
1792
+ }
1793
+
1794
+ function goalLoopNextCommandPlan(goal) {
1795
+ const command = String(goal?.next_command || '').trim();
1796
+ const attach = command.match(/^atris\s+mission\s+(attach-task|ensure-task|task-spine)\s+([A-Za-z0-9_.:-]+)\s+--json$/);
1797
+ if (attach) {
1798
+ return {
1799
+ action: 'mission_attach_task',
1800
+ command,
1801
+ args: ['mission', 'attach-task', attach[2], '--json'],
1802
+ heavy_work: false,
1803
+ setup_work: true,
1804
+ run_when_due_only: false,
1805
+ };
1806
+ }
1807
+
1808
+ const dueRun = command.match(/^atris\s+mission\s+run\s+--due\s+--max-ticks\s+([0-9]+)(\s+--complete-on-pass)?(?:\s+--json)?$/);
1809
+ if (dueRun) {
1810
+ const args = ['mission', 'run', '--due', '--max-ticks', dueRun[1]];
1811
+ if (dueRun[2]) args.push('--complete-on-pass');
1812
+ args.push('--json');
1813
+ return {
1814
+ action: 'mission_run_due',
1815
+ command: `atris ${args.join(' ')}`,
1816
+ args,
1817
+ heavy_work: true,
1818
+ setup_work: false,
1819
+ run_when_due_only: true,
1820
+ };
1821
+ }
1822
+
1823
+ return null;
1824
+ }
1825
+
1826
+ function shouldRunGoalLoopCommand(heartbeat, plan) {
1827
+ if (!heartbeat?.goal) return false;
1828
+ if (plan && plan.run_when_due_only === false) return true;
1829
+ return heartbeat.heartbeat?.due === true;
1830
+ }
1831
+
1832
+ function runMissionGoalNextCommand(root = process.cwd(), heartbeat, options = {}) {
1833
+ const plan = goalLoopNextCommandPlan(heartbeat?.goal);
1834
+ if (plan) {
1835
+ const args = [...plan.args];
1836
+ if (options.noClaude && plan.action === 'mission_run_due') args.push('--no-claude');
1837
+ return runAtrisMissionJsonCommand(root, args, {
1838
+ action: plan.action,
1839
+ heavyWork: plan.heavy_work,
1840
+ setupWork: plan.setup_work,
1841
+ });
1842
+ }
1843
+ return runMissionRunDueOnce(root, options);
1844
+ }
1845
+
1276
1846
  function sleep(ms, signal) {
1277
1847
  return new Promise((resolve, reject) => {
1278
1848
  if (ms <= 0) return resolve();
@@ -1600,7 +2170,11 @@ async function runMission(args) {
1600
2170
  const maxTicks = Math.max(1, Number(maxTicksFlag) || MISSION_RUN_DEFAULTS.maxTicks);
1601
2171
  const maxWallSeconds = Math.max(60, Number(readFlag(args, '--max-wall', '')) || MISSION_RUN_DEFAULTS.maxWallSeconds);
1602
2172
  const cadenceOverride = readFlag(args, '--cadence', '');
1603
- const ref = stripKnownFlags(args, ['--max-ticks', '--max-wall', '--cadence'], ['--json', '--due', '--no-claude', '--no-verify', '--complete-on-pass', '--no-drain'])[0] || '';
2173
+ const ref = stripKnownFlags(
2174
+ args,
2175
+ ['--max-ticks', '--max-wall', '--cadence', '--owner', '--runner', '--lane', '--verify', '--stop', '--model'],
2176
+ ['--json', '--due', '--no-claude', '--no-verify', '--complete-on-pass', '--no-drain', '--always-on', '--xp-task', '--agent-xp'],
2177
+ ).join(' ').trim();
1604
2178
 
1605
2179
  let mission = dueMode && !ref ? selectDueMission() : resolveMission(ref);
1606
2180
  if (!mission && dueMode && !ref) {
@@ -1611,8 +2185,12 @@ async function runMission(args) {
1611
2185
  );
1612
2186
  return;
1613
2187
  }
2188
+ if (!mission && ref && /\s/.test(ref) && !String(ref).startsWith('mission-')) {
2189
+ startMissionFromRunObjective(ref, args);
2190
+ return;
2191
+ }
1614
2192
  if (!mission) {
1615
- exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
2193
+ exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
1616
2194
  }
1617
2195
  if (['complete', 'stopped'].includes(mission.status)) {
1618
2196
  if (asJson) {
@@ -1752,9 +2330,11 @@ async function runMission(args) {
1752
2330
  } else if (atris2Runner) {
1753
2331
  const prompt = buildTickPrompt(mission, tickIdx, effectiveMaxTicks, frozen);
1754
2332
  const { runAtris2Turn } = require('./probe');
2333
+ const businessId = businessIdForAtris2Mission(mission, cwd);
1755
2334
  const turn = await runAtris2Turn({
1756
2335
  prompt,
1757
2336
  model: mission.model || 'atris:fast',
2337
+ business: businessId,
1758
2338
  maxTurns: 16,
1759
2339
  signal: controller.signal,
1760
2340
  });
@@ -2201,22 +2781,43 @@ function completeMission(args) {
2201
2781
  exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
2202
2782
  }
2203
2783
  const baselineSummary = pruneMissionWorktreeBaseline(mission, process.cwd());
2204
- const next = {
2784
+ const completionGate = { ...gate, forced: force && !gate.ok };
2785
+ const baseNext = {
2205
2786
  ...mission,
2206
2787
  status: 'complete',
2207
2788
  completed_at: stampIso(),
2208
2789
  proof,
2209
- completion_gate: { ...gate, forced: force && !gate.ok },
2790
+ completion_gate: completionGate,
2210
2791
  worktree_baseline: baselineSummary || mission.worktree_baseline || null,
2211
2792
  next_action: 'mission complete',
2212
2793
  };
2794
+ const xpNextCommand = missionXpReadyAction(baseNext, proof);
2795
+ const completion = missionCompletionReceipt(baseNext, proof, xpNextCommand);
2796
+ const next = {
2797
+ ...baseNext,
2798
+ landing: completion.landing,
2799
+ result: completion.result,
2800
+ };
2213
2801
  const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof, completion_gate: next.completion_gate });
2214
2802
  const logPath = appendMemberLog(saved.owner, 'Mission completed', { mission: saved.objective, proof });
2215
2803
  const codexGoalState = refreshCodexGoalController(process.cwd());
2216
- const xpNextCommand = missionXpReadyAction(saved, proof);
2217
2804
  printJsonOrText(
2218
- { ok: true, action: 'mission_completed', mission: saved, log_path: logPath, codex_goal_state: codexGoalState, xp_next_command: xpNextCommand },
2219
- [`Completed mission: ${saved.objective}`, `Proof: ${proof}`, ...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : [])],
2805
+ {
2806
+ ok: true,
2807
+ action: 'mission_completed',
2808
+ mission: saved,
2809
+ landing: completion.landing,
2810
+ result: completion.result,
2811
+ log_path: logPath,
2812
+ codex_goal_state: codexGoalState,
2813
+ xp_next_command: xpNextCommand,
2814
+ },
2815
+ [
2816
+ `Completed mission: ${saved.objective}`,
2817
+ ...missionResultLines(completion),
2818
+ `Proof: ${proof}`,
2819
+ ...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : []),
2820
+ ],
2220
2821
  asJson,
2221
2822
  );
2222
2823
  }
@@ -2311,16 +2912,27 @@ async function goalLoopMission(args) {
2311
2912
  iteration: index + 1,
2312
2913
  heartbeat,
2313
2914
  ran_heavy_work: false,
2915
+ ran_setup_work: false,
2314
2916
  dry_run: dryRun,
2315
2917
  };
2316
2918
 
2317
- if (heartbeat.heartbeat?.due && heartbeat.goal) {
2919
+ const commandPlan = goalLoopNextCommandPlan(heartbeat.goal);
2920
+ if (shouldRunGoalLoopCommand(heartbeat, commandPlan)) {
2318
2921
  if (dryRun) {
2319
2922
  event.ran_heavy_work = false;
2320
- event.run = { skipped: true, reason: 'dry-run', command: 'atris mission run --due --max-ticks 1 --complete-on-pass --json' };
2923
+ event.ran_setup_work = false;
2924
+ event.run = {
2925
+ skipped: true,
2926
+ reason: 'dry-run',
2927
+ action: commandPlan?.action || 'mission_run_due',
2928
+ command: commandPlan?.command || 'atris mission run --due --max-ticks 1 --complete-on-pass --json',
2929
+ heavy_work: commandPlan?.heavy_work === true,
2930
+ setup_work: commandPlan?.setup_work === true,
2931
+ };
2321
2932
  } else {
2322
- event.run = runMissionRunDueOnce(root, { noClaude });
2323
- event.ran_heavy_work = event.run.ok === true;
2933
+ event.run = runMissionGoalNextCommand(root, heartbeat, { noClaude });
2934
+ event.ran_heavy_work = event.run.ok === true && event.run.heavy_work === true;
2935
+ event.ran_setup_work = event.run.ok === true && event.run.setup_work === true;
2324
2936
  event.after_run = refreshCodexGoalController(root, { heartbeat: true });
2325
2937
  }
2326
2938
  }
@@ -2347,6 +2959,7 @@ async function goalLoopMission(args) {
2347
2959
  max_iterations: maxIterations,
2348
2960
  max_wall_seconds: maxWallSeconds,
2349
2961
  heavy_runs: events.filter((event) => event.ran_heavy_work).length,
2962
+ setup_runs: events.filter((event) => event.ran_setup_work).length,
2350
2963
  events,
2351
2964
  final_state: finalState,
2352
2965
  };
@@ -2373,6 +2986,8 @@ atris mission - durable goal + loop + owner + proof state
2373
2986
  default model atris:fast; runner codex_goal publishes the goal for a live
2374
2987
  Codex session to pull via atris mission goal)
2375
2988
  atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
2989
+ atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
2990
+ atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
2376
2991
  atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
2377
2992
  atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
2378
2993
  (rolls up sibling git-worktree missions; --local scopes to this checkout)
@@ -2389,7 +3004,7 @@ Autonomy recipe:
2389
3004
  1. Pick an owner member: atris member create <member> (if missing)
2390
3005
  2. Start a current-agent mission with a verifier:
2391
3006
  atris mission start "ship one proof" --owner <member> --runner codex_goal --lane code --verify "npm test" --stop "verifier passes" --xp-task
2392
- 3. Codex sessions: atris mission goal --json, then set /goal to goal.objective
3007
+ 3. Codex sessions: atris mission goal --json, then mirror goal.visible_goal into the native chat goal
2393
3008
  Overnight controller: atris mission goal --heartbeat --json
2394
3009
  Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
2395
3010
  4. Do one bounded step, then record it:
@@ -2582,6 +3197,13 @@ function missionCommand(args) {
2582
3197
  case 'list':
2583
3198
  case 'ls':
2584
3199
  return statusMission(rest);
3200
+ case 'attach-task':
3201
+ case 'ensure-task':
3202
+ case 'task-spine':
3203
+ return attachMissionTask(rest);
3204
+ case 'report':
3205
+ case 'debrief':
3206
+ return reportMission(rest);
2585
3207
  case 'watch':
2586
3208
  return watchMission(rest);
2587
3209
  case 'layers':
@@ -2625,6 +3247,7 @@ module.exports = {
2625
3247
  classifyPathsByLayer,
2626
3248
  resolveClaudeRunnerModel,
2627
3249
  resolveClaudeRunnerBin,
3250
+ businessIdForAtris2Mission,
2628
3251
  detectUnavailableModel,
2629
3252
  missionPauseNextAction,
2630
3253
  consecutiveSameReasonErrors,