atris 3.30.1 → 3.30.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.
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 +717 -32
  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
@@ -3,11 +3,16 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
+ const readline = require('readline');
6
7
  const { spawn, spawnSync } = require('child_process');
7
8
  const {
8
9
  resolveClaudeRunnerModel,
9
10
  resolveClaudeRunnerBin,
10
11
  } = require('../lib/runner-command');
12
+ const {
13
+ normalizeOwnerSlug,
14
+ resolveFunctionalOwner,
15
+ } = require('../lib/functional-owner');
11
16
 
12
17
  const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
13
18
  const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
@@ -137,6 +142,58 @@ function printJsonOrText(payload, lines, asJson) {
137
142
  for (const line of lines) console.log(line);
138
143
  }
139
144
 
145
+ function missionRunInputRequired(asJson = false) {
146
+ const payload = {
147
+ ok: false,
148
+ action: 'mission_input_required',
149
+ prompt: 'What mission should Atris run?',
150
+ owner_prompt: 'Which team member should own it?',
151
+ example: 'atris mission run "make onboarding magical" --owner mission-lead',
152
+ };
153
+ if (asJson) {
154
+ console.log(JSON.stringify(payload, null, 2));
155
+ } else {
156
+ console.error('What mission should Atris run?');
157
+ console.error('Try: atris mission run "make onboarding magical" --owner mission-lead');
158
+ }
159
+ process.exit(1);
160
+ }
161
+
162
+ function askLine(rl, question) {
163
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(String(answer || '').trim())));
164
+ }
165
+
166
+ function removeValueFlag(args, name) {
167
+ const out = [];
168
+ const prefix = `${name}=`;
169
+ for (let i = 0; i < args.length; i += 1) {
170
+ const arg = String(args[i]);
171
+ if (arg === name) {
172
+ if (args[i + 1] && !String(args[i + 1]).startsWith('--')) i += 1;
173
+ continue;
174
+ }
175
+ if (arg.startsWith(prefix)) continue;
176
+ out.push(args[i]);
177
+ }
178
+ return out;
179
+ }
180
+
181
+ async function promptMissionRunInput(args) {
182
+ const defaultOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
183
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
184
+ try {
185
+ const objective = await askLine(rl, 'Mission: ');
186
+ if (!objective) missionRunInputRequired(false);
187
+ const owner = await askLine(rl, `Team member [${defaultOwner}]: `);
188
+ return {
189
+ objective,
190
+ args: [...removeValueFlag(args, '--owner'), '--owner', owner || defaultOwner],
191
+ };
192
+ } finally {
193
+ rl.close();
194
+ }
195
+ }
196
+
140
197
  function loadTaskDb(asJson = false) {
141
198
  try {
142
199
  return require('../lib/task-db');
@@ -166,27 +223,33 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
166
223
  const db = taskDb.open();
167
224
  const workspaceRoot = taskDb.workspaceRoot(root);
168
225
  const title = `Mission XP: ${mission.objective}`;
226
+ const ownerResolution = resolveMissionOwner(mission, workspaceRoot);
227
+ const owner = ownerResolution.owner;
169
228
  const metadata = {
170
- assigned_to: mission.owner,
229
+ assigned_to: owner,
171
230
  delegate_via: 'mission_goal_loop',
172
231
  created_for_day: todayName(),
173
232
  goal_id: mission.id,
174
233
  goal_objective: mission.objective,
175
234
  mission_id: mission.id,
176
235
  mission_objective: mission.objective,
177
- mission_owner: mission.owner,
236
+ mission_owner: owner,
237
+ owner_resolution: ownerResolution.reason,
178
238
  mission_lane: mission.lane,
179
239
  mission_runner: mission.runner,
180
240
  verify: mission.verifier || null,
181
241
  stop_condition: mission.stop_condition || null,
182
242
  };
243
+ if (ownerResolution.requested_owner && ownerResolution.requested_owner !== owner) metadata.requested_owner = ownerResolution.requested_owner;
244
+ if (ownerResolution.executed_by) metadata.executed_by = normalizeOwnerSlug(ownerResolution.executed_by);
245
+ if (ownerResolution.proposed_member) metadata.proposed_member = ownerResolution.proposed_member;
183
246
  const result = taskDb.addTask(db, {
184
247
  title,
185
248
  tag: 'agent-xp',
186
249
  workspaceRoot,
187
250
  sourceKey: `mission-xp:${mission.id}`,
188
251
  status: 'claimed',
189
- claimedBy: mission.owner,
252
+ claimedBy: owner,
190
253
  metadata,
191
254
  });
192
255
  const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
@@ -194,7 +257,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
194
257
  if (task) {
195
258
  taskDb.noteTask(db, {
196
259
  id: task.id,
197
- actor: process.env.ATRIS_AGENT_ID || mission.owner || 'mission-lead',
260
+ actor: process.env.ATRIS_AGENT_ID || owner,
198
261
  content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP lands only after human accept.`,
199
262
  });
200
263
  }
@@ -204,7 +267,9 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
204
267
  ref: missionTaskRef(task) || result.id,
205
268
  title,
206
269
  status: task?.status || 'claimed',
207
- assigned_to: mission.owner,
270
+ assigned_to: owner,
271
+ owner_resolution: ownerResolution.reason,
272
+ executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
208
273
  inserted: result.inserted !== false,
209
274
  projection_path: outPath,
210
275
  };
@@ -223,6 +288,24 @@ function statePaths(root = process.cwd()) {
223
288
  };
224
289
  }
225
290
 
291
+ function readBusinessBinding(root = process.cwd()) {
292
+ const file = path.join(root, '.atris', 'business.json');
293
+ try {
294
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
295
+ return {
296
+ business_id: parsed.business_id || '',
297
+ workspace_id: parsed.workspace_id || '',
298
+ slug: parsed.slug || '',
299
+ };
300
+ } catch {
301
+ return null;
302
+ }
303
+ }
304
+
305
+ function businessIdForAtris2Mission(mission, cwd = process.cwd()) {
306
+ return mission?.business_id || readBusinessBinding(cwd)?.business_id || null;
307
+ }
308
+
226
309
  function readJsonLines(file) {
227
310
  if (!fs.existsSync(file)) return [];
228
311
  return fs.readFileSync(file, 'utf8')
@@ -424,6 +507,7 @@ function renderMemberNowMarkdown(owner, missions) {
424
507
  return lines.join('\n');
425
508
  }
426
509
  for (const mission of missions) {
510
+ const taskSpine = missionTaskSpine(mission);
427
511
  lines.push(`## ${mission.objective}`);
428
512
  lines.push('');
429
513
  lines.push(`- id: ${mission.id}`);
@@ -431,6 +515,9 @@ function renderMemberNowMarkdown(owner, missions) {
431
515
  lines.push(`- cadence: ${mission.cadence}`);
432
516
  lines.push(`- runner: ${mission.runner}${mission.model ? ` (${mission.model})` : ''}`);
433
517
  lines.push(`- lane: ${mission.lane}`);
518
+ if (taskSpine.task_ref) lines.push(`- task: ${taskSpine.task_ref}`);
519
+ if (taskSpine.current_step_command) lines.push(`- task next: ${taskSpine.current_step_command}`);
520
+ if (!taskSpine.has_task && taskSpine.ensure_task_command) lines.push(`- task setup: ${taskSpine.ensure_task_command}`);
434
521
  if (mission.xp_task?.ref) lines.push(`- AgentXP task: ${mission.xp_task.ref}`);
435
522
  if (mission.verifier) lines.push(`- verifier: ${mission.verifier}`);
436
523
  if (mission.stop_condition) lines.push(`- stop: ${mission.stop_condition}`);
@@ -463,6 +550,52 @@ function completionGateLabel(gate) {
463
550
  return gate.forced ? `forced override (${gate.source})` : gate.source;
464
551
  }
465
552
 
553
+ function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
554
+ const gate = mission.completion_gate || {};
555
+ const gateLabel = completionGateLabel(gate) || 'completion gate';
556
+ const checked = gate.source === 'receipt' && gate.receipt_path
557
+ ? `I checked the passing verifier receipt ${gate.receipt_path}.`
558
+ : gate.source === 'mission_state'
559
+ ? 'I checked mission state showing the verifier passed.'
560
+ : gate.source === 'no_verifier'
561
+ ? 'No verifier was configured, so completion used the no-verifier gate.'
562
+ : `I checked the ${gateLabel} completion gate.`;
563
+ const tested = mission.verifier_result?.passed
564
+ ? `Verifier passed: ${mission.verifier_result.command || mission.verifier || 'configured verifier'}.`
565
+ : mission.verifier
566
+ ? `Completion proof is attached for verifier: ${mission.verifier}.`
567
+ : 'No verifier command was recorded for this mission.';
568
+ const landing = {
569
+ happened: `Mission completed: ${mission.objective}.`,
570
+ checked,
571
+ tested,
572
+ saved: `Saved complete mission ${mission.id}${proof ? ` with proof ${proof}` : ''}.`,
573
+ decision: xpNextCommand
574
+ ? `Queue AgentXP next: ${xpNextCommand}`
575
+ : 'Mission is complete; start or resume the next mission if more work remains.',
576
+ };
577
+ const result = {
578
+ changed: landing.happened,
579
+ checked: landing.checked,
580
+ tested: landing.tested,
581
+ saved: landing.saved,
582
+ accept: landing.decision,
583
+ };
584
+ return { landing, result };
585
+ }
586
+
587
+ function missionResultLines(completion) {
588
+ const landing = completion?.landing || {};
589
+ const result = completion?.result || {};
590
+ const lines = ['Result:'];
591
+ if (landing.happened) lines.push(` What happened: ${landing.happened}`);
592
+ if (landing.checked) lines.push(` How I checked: ${landing.checked}`);
593
+ if (landing.tested) lines.push(` What I tested: ${landing.tested}`);
594
+ if (result.saved) lines.push(` Saved: ${result.saved}`);
595
+ if (landing.decision) lines.push(` Decision: ${landing.decision}`);
596
+ return lines;
597
+ }
598
+
466
599
  function renderMissionStatus(root = process.cwd()) {
467
600
  const paths = statePaths(root);
468
601
  const missions = listMissions(root);
@@ -478,10 +611,14 @@ function renderMissionStatus(root = process.cwd()) {
478
611
  lines.push('No missions yet.', '');
479
612
  } else {
480
613
  for (const mission of missions.slice(0, 12)) {
614
+ const taskSpine = missionTaskSpine(mission);
481
615
  lines.push(`- **${mission.id}** ${mission.objective}`);
482
616
  lines.push(` - owner: ${mission.owner}`);
483
617
  lines.push(` - state: ${mission.status}`);
484
618
  lines.push(` - next: ${mission.next_action || 'tick or verify'}`);
619
+ if (taskSpine?.task_ref) lines.push(` - task: ${taskSpine.task_ref}`);
620
+ if (taskSpine?.current_step_command) lines.push(` - task next: ${taskSpine.current_step_command}`);
621
+ if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) lines.push(` - task setup: ${taskSpine.ensure_task_command}`);
485
622
  if (mission.xp_task?.ref) lines.push(` - AgentXP task: ${mission.xp_task.ref}`);
486
623
  if (mission.receipt_path) lines.push(` - proof: ${mission.receipt_path}`);
487
624
  const gateLabel = completionGateLabel(mission.completion_gate);
@@ -501,13 +638,110 @@ function missionXpTaskRefFromMission(mission) {
501
638
  return '';
502
639
  }
503
640
 
641
+ function resolveMissionOwner(mission, root = process.cwd()) {
642
+ const requestedOwner = mission?.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
643
+ const resolved = resolveFunctionalOwner({
644
+ requestedOwner,
645
+ title: mission?.objective || '',
646
+ tag: mission?.lane || 'mission',
647
+ note: mission?.next_action || '',
648
+ goal: mission?.objective || '',
649
+ root,
650
+ fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
651
+ });
652
+ const requested = mission?.requested_owner || (
653
+ resolved.requested_owner && resolved.requested_owner !== resolved.owner ? resolved.requested_owner : null
654
+ );
655
+ return {
656
+ ...resolved,
657
+ reason: mission?.owner_resolution || resolved.reason,
658
+ requested_owner: requested,
659
+ executed_by: mission?.executed_by || resolved.executed_by || null,
660
+ };
661
+ }
662
+
663
+ function applyMissionOwnerResolution(mission, root = process.cwd()) {
664
+ const ownerResolution = resolveMissionOwner(mission, root);
665
+ const next = {
666
+ ...mission,
667
+ owner: ownerResolution.owner,
668
+ owner_resolution: ownerResolution.reason,
669
+ };
670
+ if (ownerResolution.requested_owner && ownerResolution.requested_owner !== ownerResolution.owner) {
671
+ next.requested_owner = ownerResolution.requested_owner;
672
+ }
673
+ if (ownerResolution.executed_by) {
674
+ next.executed_by = normalizeOwnerSlug(ownerResolution.executed_by);
675
+ }
676
+ if (ownerResolution.proposed_member) {
677
+ next.proposed_member = ownerResolution.proposed_member;
678
+ }
679
+ return { mission: next, ownerResolution };
680
+ }
681
+
504
682
  function missionXpReadyAction(mission, receiptPath) {
505
683
  const ref = missionXpTaskRefFromMission(mission);
506
684
  if (!ref || !receiptPath) return null;
507
- const owner = mission.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
685
+ const owner = resolveMissionOwner(mission).owner;
508
686
  return `queue AgentXP review: atris task current-step --goal-id ${mission.id} --as ${owner} --proof "${receiptPath}" --json`;
509
687
  }
510
688
 
689
+ function missionTaskSpine(mission) {
690
+ if (!mission || !mission.id) return null;
691
+ const ownerResolution = resolveMissionOwner(mission);
692
+ const taskIds = Array.isArray(mission.task_ids) ? mission.task_ids.filter(Boolean) : [];
693
+ const taskId = mission.xp_task?.task_id
694
+ || mission.current_task_id
695
+ || mission.task_id
696
+ || taskIds[0]
697
+ || null;
698
+ const taskRef = mission.xp_task?.ref
699
+ || mission.task_ref
700
+ || (taskId ? String(taskId) : null);
701
+ const owner = ownerResolution.owner;
702
+ return {
703
+ schema: 'atris.mission_task_spine.v1',
704
+ goal_id: mission.id,
705
+ owner,
706
+ requested_owner: ownerResolution.requested_owner || null,
707
+ owner_resolution: ownerResolution.reason,
708
+ executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
709
+ lane: mission.lane || 'workspace',
710
+ runner: mission.runner || 'manual',
711
+ task_id: taskId,
712
+ current_task_id: taskId,
713
+ task_ref: taskRef,
714
+ has_task: Boolean(taskId || taskRef),
715
+ current_step_command: taskId || taskRef
716
+ ? `atris task current-step --goal-id ${mission.id} --as ${owner} --proof "<proof>" --json`
717
+ : null,
718
+ ensure_task_command: taskId || taskRef
719
+ ? null
720
+ : `atris mission attach-task ${mission.id} --json`,
721
+ };
722
+ }
723
+
724
+ function missionStatusView(mission) {
725
+ const taskSpine = missionTaskSpine(mission);
726
+ if (!taskSpine) return mission;
727
+ const requestedOwner = taskSpine.requested_owner
728
+ || mission.requested_owner
729
+ || (mission.owner && mission.owner !== taskSpine.owner ? mission.owner : null);
730
+ return {
731
+ ...mission,
732
+ owner: taskSpine.owner,
733
+ functional_owner: taskSpine.owner,
734
+ requested_owner: requestedOwner,
735
+ owner_resolution: taskSpine.owner_resolution,
736
+ executed_by: taskSpine.executed_by || mission.executed_by || null,
737
+ goal_id: taskSpine.goal_id,
738
+ task_id: taskSpine.task_id,
739
+ current_task_id: taskSpine.current_task_id,
740
+ task_ref: taskSpine.task_ref,
741
+ task_spine: taskSpine,
742
+ };
743
+ }
744
+
511
745
  function missionFromArgs(args) {
512
746
  const objective = stripKnownFlags(args, [
513
747
  '--owner',
@@ -524,11 +758,20 @@ function missionFromArgs(args) {
524
758
  if (!objective) {
525
759
  exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
526
760
  }
527
- const owner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
761
+ const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
528
762
  const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
529
763
  const runner = readFlag(args, '--runner', 'manual');
530
764
  const model = readFlag(args, '--model', '') || (String(runner).toLowerCase() === 'atris2' ? 'atris:fast' : '');
531
765
  const lane = readFlag(args, '--lane', 'workspace');
766
+ const ownerResolution = resolveFunctionalOwner({
767
+ requestedOwner,
768
+ title: objective,
769
+ tag: lane,
770
+ goal: objective,
771
+ root: process.cwd(),
772
+ fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
773
+ });
774
+ const owner = ownerResolution.owner;
532
775
  const verifier = readFlag(args, '--verify', '');
533
776
  assertMissionVerifier(verifier, wantsJson(args));
534
777
  const stopCondition = readFlag(args, '--stop', verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof');
@@ -536,6 +779,7 @@ function missionFromArgs(args) {
536
779
  const humanAsks = readRepeatedFlag(args, '--ask');
537
780
  const alwaysOn = hasFlag(args, '--always-on');
538
781
  const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
782
+ const businessBinding = readBusinessBinding(process.cwd());
539
783
  const id = missionId(objective);
540
784
  const mission = {
541
785
  schema: 'atris.mission.v1',
@@ -543,10 +787,16 @@ function missionFromArgs(args) {
543
787
  slug: slugify(objective),
544
788
  objective,
545
789
  owner,
790
+ owner_resolution: ownerResolution.reason,
791
+ ...(ownerResolution.requested_owner && ownerResolution.requested_owner !== owner ? { requested_owner: ownerResolution.requested_owner } : {}),
792
+ ...(ownerResolution.executed_by ? { executed_by: normalizeOwnerSlug(ownerResolution.executed_by) } : {}),
793
+ ...(ownerResolution.proposed_member ? { proposed_member: ownerResolution.proposed_member } : {}),
546
794
  status: 'planning',
547
795
  cadence,
548
796
  runner,
549
797
  ...(model ? { model } : {}),
798
+ ...(businessBinding?.business_id ? { business_id: businessBinding.business_id } : {}),
799
+ ...(businessBinding?.workspace_id ? { workspace_id: businessBinding.workspace_id } : {}),
550
800
  lane,
551
801
  verifier,
552
802
  always_on: alwaysOn,
@@ -571,6 +821,36 @@ function missingVerifierWarning(mission) {
571
821
  };
572
822
  }
573
823
 
824
+ function missionRunSmokeVerifier() {
825
+ const script = [
826
+ "const fs=require('fs')",
827
+ "const os=require('os')",
828
+ "const path=require('path')",
829
+ "const {spawnSync}=require('child_process')",
830
+ "const root=fs.mkdtempSync(path.join(os.tmpdir(),'atris-mission-run-verifier-'))",
831
+ "process.on('exit',()=>fs.rmSync(root,{recursive:true,force:true}))",
832
+ "fs.mkdirSync(path.join(root,'atris'),{recursive:true})",
833
+ "const r=spawnSync('atris',['mission','run','verifier smoke objective','--json'],{cwd:root,encoding:'utf8',env:{...process.env,ATRIS_SKIP_UPDATE_CHECK:'1'}})",
834
+ "if(r.status!==0){process.stderr.write(r.stderr||r.stdout);process.exit(1)}",
835
+ "const p=JSON.parse(r.stdout)",
836
+ "if(p.action!=='mission_run_started')process.exit(2)",
837
+ "if(p.mission?.runner!=='codex_goal')process.exit(3)",
838
+ "if(p.codex_goal_state?.goal?.visible_goal?.schema!=='atris.visible_chat_goal_bridge.v1')process.exit(4)",
839
+ ].join(';');
840
+ return `node -e ${JSON.stringify(script)}`;
841
+ }
842
+
843
+ function inferRunObjectiveVerifier(objective, root = process.cwd()) {
844
+ const text = String(objective || '').toLowerCase();
845
+ if (!/\bmission\s+run\b/.test(text)) return '';
846
+ const sourceTest = path.join(root, 'test', 'mission-status.test.js');
847
+ const sourceCommand = path.join(root, 'commands', 'mission.js');
848
+ if (fs.existsSync(sourceTest) && fs.existsSync(sourceCommand)) {
849
+ return 'node --test test/mission-status.test.js';
850
+ }
851
+ return missionRunSmokeVerifier();
852
+ }
853
+
574
854
  function startMission(args) {
575
855
  const asJson = wantsJson(args);
576
856
  const mission = missionFromArgs(args);
@@ -624,6 +904,137 @@ function startMission(args) {
624
904
  );
625
905
  }
626
906
 
907
+ function startMissionFromRunObjective(objective, args) {
908
+ const asJson = wantsJson(args);
909
+ const verifier = readFlag(args, '--verify', inferRunObjectiveVerifier(objective));
910
+ const stopCondition = readFlag(
911
+ args,
912
+ '--stop',
913
+ verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready',
914
+ );
915
+ const startArgs = [
916
+ objective,
917
+ '--owner',
918
+ readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead'),
919
+ '--runner',
920
+ readFlag(args, '--runner', 'codex_goal'),
921
+ '--lane',
922
+ readFlag(args, '--lane', 'workspace'),
923
+ '--cadence',
924
+ readFlag(args, '--cadence', 'manual'),
925
+ '--stop',
926
+ stopCondition,
927
+ ];
928
+ if (verifier) startArgs.push('--verify', verifier);
929
+ const model = readFlag(args, '--model', '');
930
+ if (model) startArgs.push('--model', model);
931
+ if (hasFlag(args, '--always-on')) startArgs.push('--always-on');
932
+ if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp')) startArgs.push('--xp-task');
933
+
934
+ const mission = missionFromArgs(startArgs);
935
+ const warnings = [missingVerifierWarning(mission)].filter(Boolean);
936
+ ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
937
+ const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
938
+ const memberState = renderMemberMissionState(saved.owner);
939
+ const logPath = appendMemberLog(saved.owner, 'Mission started from run', {
940
+ mission: saved.objective,
941
+ cadence: saved.cadence,
942
+ runner: saved.runner,
943
+ lane: saved.lane,
944
+ verifier: saved.verifier,
945
+ });
946
+ const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
947
+ const codexGoalState = refreshCodexGoalController(process.cwd());
948
+ printJsonOrText(
949
+ {
950
+ ok: true,
951
+ action: 'mission_run_started',
952
+ mission: saved,
953
+ warnings,
954
+ state_path: statePaths().missionsJsonl,
955
+ member_state: memberState,
956
+ log_path: logPath,
957
+ worktree_baseline: worktreeBaseline ? {
958
+ path: path.relative(process.cwd(), missionBaselinePath(saved.id)),
959
+ dirty_count: worktreeBaseline.dirty_count,
960
+ dirty_hash: worktreeBaseline.dirty_hash,
961
+ } : null,
962
+ codex_goal_state: codexGoalState,
963
+ next_command: codexGoalState.goal?.next_command || `atris mission tick ${saved.id} --verify`,
964
+ },
965
+ [
966
+ `Started mission: ${saved.objective}`,
967
+ `Owner: ${saved.owner}`,
968
+ `Runner: ${saved.runner}`,
969
+ ...(codexGoalState.goal ? [`Visible goal: ${codexGoalState.goal.objective}`] : []),
970
+ ...warnings.map((warning) => `Warning: ${warning.message}`),
971
+ ],
972
+ asJson,
973
+ );
974
+ }
975
+
976
+ function attachMissionTask(args) {
977
+ const asJson = wantsJson(args);
978
+ const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
979
+ if (!ref) {
980
+ exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
981
+ }
982
+ const mission = resolveMission(ref);
983
+ if (!mission) {
984
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
985
+ }
986
+ if (TERMINAL_STATUSES.has(mission.status)) {
987
+ exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
988
+ }
989
+
990
+ const existingSpine = missionTaskSpine(mission);
991
+ if (existingSpine?.has_task) {
992
+ const view = missionStatusView(mission);
993
+ printJsonOrText(
994
+ { ok: true, action: 'mission_task_spine_exists', mission: view, task_spine: view.task_spine },
995
+ [
996
+ `Mission task spine already exists: ${mission.objective}`,
997
+ `Task: ${view.task_spine.task_ref}`,
998
+ `Next: ${view.task_spine.current_step_command}`,
999
+ ],
1000
+ asJson,
1001
+ );
1002
+ return;
1003
+ }
1004
+
1005
+ const ownership = applyMissionOwnerResolution(mission, process.cwd());
1006
+ const baseMission = ownership.mission;
1007
+ const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
1008
+ const nextMission = {
1009
+ ...baseMission,
1010
+ xp_task_enabled: true,
1011
+ xp_task: xpTask,
1012
+ task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
1013
+ };
1014
+ if (nextMission.status === 'ready' && nextMission.receipt_path) {
1015
+ nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
1016
+ } else if (!nextMission.verifier && !nextMission.always_on) {
1017
+ nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
1018
+ }
1019
+
1020
+ const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
1021
+ const memberState = renderMemberMissionState(saved.owner);
1022
+ const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
1023
+ mission: saved.objective,
1024
+ task: xpTask.ref,
1025
+ });
1026
+ const view = missionStatusView(saved);
1027
+ printJsonOrText(
1028
+ { ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
1029
+ [
1030
+ `Attached task spine: ${saved.objective}`,
1031
+ `Task: ${xpTask.ref}`,
1032
+ `Next: ${view.task_spine.current_step_command}`,
1033
+ ],
1034
+ asJson,
1035
+ );
1036
+ }
1037
+
627
1038
  function statusMission(args) {
628
1039
  const asJson = wantsJson(args);
629
1040
  const localOnly = hasFlag(args, '--local');
@@ -648,14 +1059,15 @@ function statusMission(args) {
648
1059
  if (ref && !missions.length) {
649
1060
  exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
650
1061
  }
1062
+ const missionViews = missions.map(missionStatusView);
651
1063
  // 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))) {
1064
+ for (const owner of new Set(missionViews.filter((mission) => !mission.worktree_root).map((mission) => mission.owner).filter(Boolean))) {
653
1065
  renderMemberMissionState(owner);
654
1066
  }
655
1067
  const payload = {
656
1068
  ok: true,
657
1069
  action: 'mission_status',
658
- missions,
1070
+ missions: missionViews,
659
1071
  state_path: statePaths().missionsJsonl,
660
1072
  events_path: statePaths().eventsJsonl,
661
1073
  status_path: renderMissionStatus(),
@@ -663,14 +1075,18 @@ function statusMission(args) {
663
1075
  printJsonOrText(
664
1076
  payload,
665
1077
  missions.length
666
- ? missions.flatMap((mission) => [
1078
+ ? missionViews.flatMap((mission) => [
667
1079
  `Mission: ${mission.objective}`,
668
1080
  ` id: ${mission.id}`,
669
1081
  ` owner: ${mission.owner}`,
1082
+ ...(mission.executed_by ? [` executed_by: ${mission.executed_by}`] : []),
670
1083
  ` state: ${mission.status}`,
671
1084
  ...missionHeartbeatLines(mission),
672
1085
  ...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
673
1086
  ` next: ${mission.next_action || 'tick or verify'}`,
1087
+ ...(mission.task_spine?.task_ref ? [` task: ${mission.task_spine.task_ref}`] : []),
1088
+ ...(mission.task_spine?.current_step_command ? [` task next: ${mission.task_spine.current_step_command}`] : []),
1089
+ ...(!mission.task_spine?.has_task && mission.task_spine?.ensure_task_command ? [` task setup: ${mission.task_spine.ensure_task_command}`] : []),
674
1090
  ...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
675
1091
  ...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
676
1092
  ])
@@ -679,6 +1095,108 @@ function statusMission(args) {
679
1095
  );
680
1096
  }
681
1097
 
1098
+ function readMissionReceipt(receiptPath, root = process.cwd()) {
1099
+ if (!receiptPath) return null;
1100
+ const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
1101
+ try {
1102
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
1103
+ } catch {
1104
+ return null;
1105
+ }
1106
+ }
1107
+
1108
+ function firstUsefulLine(text, fallback = '') {
1109
+ return String(text || '')
1110
+ .split(/\r?\n/)
1111
+ .map((line) => line.replace(/^[-*\s#]+/, '').trim())
1112
+ .filter(Boolean)
1113
+ .find((line) => !/^(receipt|summary|final|result)$/i.test(line))
1114
+ || fallback;
1115
+ }
1116
+
1117
+ function missionWorkerLabel(mission) {
1118
+ const runner = String(mission && mission.runner || 'manual').toLowerCase();
1119
+ if (runner === 'atris2') return `Remote Atris2 computer${mission.model ? ` using ${mission.model}` : ''}`;
1120
+ if (runner === 'claude') return `Claude worker${mission.model ? ` using ${mission.model}` : ''}`;
1121
+ if (runner === 'codex_goal') return 'Codex goal handoff';
1122
+ return 'Local mission tick';
1123
+ }
1124
+
1125
+ function missionWorkerSummary(mission, receipt) {
1126
+ if (mission && mission.worker_summary) return mission.worker_summary;
1127
+ const tick = receipt && receipt.result && (receipt.result.tick || (Array.isArray(receipt.result.ticks) ? receipt.result.ticks[receipt.result.ticks.length - 1] : null));
1128
+ if (!tick) return mission && mission.last_tick_reason ? `Last tick: ${mission.last_tick_reason}` : 'No worker receipt yet.';
1129
+ if (tick.atris2) {
1130
+ return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Remote worker ran and returned a response.' : 'Remote worker failed.');
1131
+ }
1132
+ if (tick.claude) {
1133
+ if (tick.claude.skipped) {
1134
+ return tick.summary || `Worker step skipped: ${tick.claude.reason || tick.reason || 'not needed'}.`;
1135
+ }
1136
+ return tick.claude.summary || firstUsefulLine(tick.claude.receipt_text, tick.claude.ok ? 'Worker ran and returned a response.' : 'Worker failed.');
1137
+ }
1138
+ if (tick.summary) return tick.summary;
1139
+ return tick.reason ? `Worker tick: ${tick.reason}` : 'Worker tick recorded.';
1140
+ }
1141
+
1142
+ function missionReportFor(mission, root = process.cwd()) {
1143
+ const verifierReceiptPath = mission.receipt_path || null;
1144
+ const receipt = readMissionReceipt(verifierReceiptPath, root);
1145
+ const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
1146
+ const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
1147
+ const operatorOutcome = mission.operator_outcome
1148
+ || (verifierPassed ? 'Verifier passed.' : mission.status === 'complete' ? 'Mission is complete.' : mission.status === 'blocked' ? 'Mission is blocked.' : 'Mission is still in progress.');
1149
+ return {
1150
+ id: mission.id,
1151
+ objective: mission.objective,
1152
+ status: mission.status,
1153
+ operator_outcome: operatorOutcome,
1154
+ worker: mission.worker || missionWorkerLabel(mission),
1155
+ worker_summary: missionWorkerSummary(mission, receipt),
1156
+ worker_receipt_path: workerReceiptPath,
1157
+ verifier_receipt_path: verifierReceiptPath,
1158
+ operator_next: mission.operator_next || mission.next_action || 'Review the mission state.',
1159
+ };
1160
+ }
1161
+
1162
+ function reportMission(args) {
1163
+ const asJson = wantsJson(args);
1164
+ const localOnly = hasFlag(args, '--local');
1165
+ const ref = stripKnownFlags(args, ['--limit'], ['--json', '--local'])[0] || '';
1166
+ const limit = readPositiveIntegerFlag(args, '--limit', ref ? 1 : 3, { json: asJson });
1167
+ let missions = ref ? [resolveMission(ref)].filter(Boolean) : listMissions();
1168
+ if (!ref && !localOnly) {
1169
+ const seen = new Set(missions.map((mission) => mission.id));
1170
+ for (const rolled of listWorktreeRollupMissions()) {
1171
+ if (seen.has(rolled.id)) continue;
1172
+ seen.add(rolled.id);
1173
+ missions.push(rolled);
1174
+ }
1175
+ missions.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
1176
+ }
1177
+ if (ref && !missions.length) {
1178
+ exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
1179
+ }
1180
+ missions = missions.slice(0, limit);
1181
+ const reports = missions.map((mission) => missionReportFor(mission, mission.worktree_root || process.cwd()));
1182
+ printJsonOrText(
1183
+ { ok: true, action: 'mission_report', reports },
1184
+ reports.length
1185
+ ? reports.flatMap((report) => [
1186
+ `Mission: ${report.objective}`,
1187
+ ` state: ${report.status}`,
1188
+ ` What happened: ${report.operator_outcome}`,
1189
+ ` Worker: ${report.worker}`,
1190
+ ` Worker summary: ${report.worker_summary}`,
1191
+ ...(report.worker_receipt_path ? [` Worker receipt: ${report.worker_receipt_path}`] : []),
1192
+ ...(report.verifier_receipt_path ? [` Verifier receipt: ${report.verifier_receipt_path}`] : []),
1193
+ ` Next: ${report.operator_next}`,
1194
+ ])
1195
+ : ['No missions yet. Run: atris mission start "..." --owner <member>'],
1196
+ asJson,
1197
+ );
1198
+ }
1199
+
682
1200
  // `atris mission watch [id]` — read-only live heartbeat. Prints a line per tick as it
683
1201
  // lands so a human (or any terminal) can see the loop is alive without rerunning status.
684
1202
  function watchMission(args) {
@@ -1125,10 +1643,14 @@ function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
1125
1643
  }
1126
1644
 
1127
1645
  function codexGoalObjective(mission) {
1128
- return `Advance Atris mission ${mission.id}: ${mission.objective}`;
1646
+ return mission.objective;
1129
1647
  }
1130
1648
 
1131
1649
  function codexGoalNextCommand(mission) {
1650
+ const taskSpine = missionTaskSpine(mission);
1651
+ if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) {
1652
+ return taskSpine.ensure_task_command;
1653
+ }
1132
1654
  if (mission.status === 'ready') {
1133
1655
  const xpAction = missionXpReadyAction(mission, mission.receipt_path);
1134
1656
  if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
@@ -1143,14 +1665,40 @@ function codexGoalNextCommand(mission) {
1143
1665
  return `atris mission tick ${mission.id} --summary "<what changed>"`;
1144
1666
  }
1145
1667
 
1668
+ function codexVisibleGoalBridge(mission, goalObjective) {
1669
+ return {
1670
+ schema: 'atris.visible_chat_goal_bridge.v1',
1671
+ runtime: 'codex',
1672
+ source: 'atris_mission',
1673
+ mission_id: mission.id,
1674
+ desired_objective: goalObjective,
1675
+ status: 'needs_runtime_write',
1676
+ state_file: '.atris/state/codex_goal.json',
1677
+ status_file: 'atris/status/codex-goal.md',
1678
+ operations: {
1679
+ read_current_goal: 'get_goal',
1680
+ keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
1681
+ create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
1682
+ complete_after_proof: 'update_goal({ status: "complete" })',
1683
+ refresh_next_candidate: 'atris mission goal --json',
1684
+ },
1685
+ guardrails: [
1686
+ 'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
1687
+ 'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
1688
+ ],
1689
+ };
1690
+ }
1691
+
1146
1692
  function codexGoalToolContract(mission) {
1147
1693
  return {
1148
1694
  current_policy: 'keep one visible Codex /goal active for the selected Atris mission',
1149
1695
  read_current_goal: 'get_goal',
1150
1696
  complete_current_goal: 'update_goal({ status: "complete" })',
1151
1697
  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.',
1698
+ set_next_goal: 'use goal.visible_goal: create_goal({ objective: goal.objective }) when no active goal blocks the slot',
1699
+ visible_goal_bridge: 'goal.visible_goal',
1700
+ platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
1701
+ runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof -> atris mission goal --json -> create_goal({ objective: goal.objective })',
1154
1702
  blocked_without_platform_goal_write: true,
1155
1703
  mission_id: mission.id,
1156
1704
  };
@@ -1191,6 +1739,12 @@ function writeCodexGoalState(payload, root = process.cwd()) {
1191
1739
  lines.push(`- reason: ${state.goal.reason}`);
1192
1740
  lines.push(`- objective: ${state.goal.objective}`);
1193
1741
  lines.push(`- next: ${state.goal.next_command}`);
1742
+ if (state.goal.visible_goal) {
1743
+ lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
1744
+ lines.push(`- visible goal desired: ${state.goal.visible_goal.desired_objective}`);
1745
+ lines.push(`- visible goal create: ${state.goal.visible_goal.operations.create_when_empty_or_completed}`);
1746
+ lines.push(`- visible goal complete: ${state.goal.visible_goal.operations.complete_after_proof}`);
1747
+ }
1194
1748
  lines.push(`- platform write blocked: ${state.goal.codex_tool_contract.blocked_without_platform_goal_write}`);
1195
1749
  } else {
1196
1750
  lines.push('- mission: none');
@@ -1219,14 +1773,21 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1219
1773
  }
1220
1774
 
1221
1775
  const { mission, reason } = selected;
1776
+ const taskSpine = missionTaskSpine(mission);
1777
+ const missionView = missionStatusView(mission);
1778
+ const objective = codexGoalObjective(mission);
1222
1779
  const goal = {
1223
- objective: codexGoalObjective(mission),
1780
+ objective,
1224
1781
  mission_id: mission.id,
1225
1782
  mission_objective: mission.objective,
1226
1783
  mission_status: mission.status,
1784
+ owner: taskSpine?.owner || mission.owner,
1785
+ executed_by: taskSpine?.executed_by || mission.executed_by || null,
1786
+ task_spine: taskSpine,
1227
1787
  reason,
1228
1788
  next_command: codexGoalNextCommand(mission),
1229
1789
  replace_after: 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.',
1790
+ visible_goal: codexVisibleGoalBridge(mission, objective),
1230
1791
  codex_tool_contract: codexGoalToolContract(mission),
1231
1792
  };
1232
1793
  const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
@@ -1234,7 +1795,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
1234
1795
  ok: true,
1235
1796
  action: heartbeatMode ? 'codex_goal_heartbeat' : 'codex_goal_candidate',
1236
1797
  goal,
1237
- mission,
1798
+ mission: missionView,
1238
1799
  heartbeat,
1239
1800
  };
1240
1801
  }
@@ -1249,10 +1810,8 @@ function refreshCodexGoalController(root = process.cwd(), options = {}) {
1249
1810
  };
1250
1811
  }
1251
1812
 
1252
- function runMissionRunDueOnce(root = process.cwd(), options = {}) {
1813
+ function runAtrisMissionJsonCommand(root, args, options = {}) {
1253
1814
  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
1815
  const result = spawnSync(process.execPath, [cliPath, ...args], {
1257
1816
  cwd: root,
1258
1817
  encoding: 'utf8',
@@ -1264,15 +1823,79 @@ function runMissionRunDueOnce(root = process.cwd(), options = {}) {
1264
1823
  payload = JSON.parse(result.stdout || '{}');
1265
1824
  } catch {}
1266
1825
  return {
1826
+ action: options.action || null,
1267
1827
  command: `atris ${args.join(' ')}`,
1268
1828
  status: result.status,
1269
1829
  ok: result.status === 0,
1830
+ heavy_work: options.heavyWork === true,
1831
+ setup_work: options.setupWork === true,
1270
1832
  stdout: String(result.stdout || '').slice(-4000),
1271
1833
  stderr: String(result.stderr || '').slice(-4000),
1272
1834
  payload,
1273
1835
  };
1274
1836
  }
1275
1837
 
1838
+ function runMissionRunDueOnce(root = process.cwd(), options = {}) {
1839
+ const args = ['mission', 'run', '--due', '--max-ticks', '1', '--complete-on-pass', '--json'];
1840
+ if (options.noClaude) args.push('--no-claude');
1841
+ return runAtrisMissionJsonCommand(root, args, {
1842
+ action: 'mission_run_due',
1843
+ heavyWork: true,
1844
+ });
1845
+ }
1846
+
1847
+ function goalLoopNextCommandPlan(goal) {
1848
+ const command = String(goal?.next_command || '').trim();
1849
+ const attach = command.match(/^atris\s+mission\s+(attach-task|ensure-task|task-spine)\s+([A-Za-z0-9_.:-]+)\s+--json$/);
1850
+ if (attach) {
1851
+ return {
1852
+ action: 'mission_attach_task',
1853
+ command,
1854
+ args: ['mission', 'attach-task', attach[2], '--json'],
1855
+ heavy_work: false,
1856
+ setup_work: true,
1857
+ run_when_due_only: false,
1858
+ };
1859
+ }
1860
+
1861
+ const dueRun = command.match(/^atris\s+mission\s+run\s+--due\s+--max-ticks\s+([0-9]+)(\s+--complete-on-pass)?(?:\s+--json)?$/);
1862
+ if (dueRun) {
1863
+ const args = ['mission', 'run', '--due', '--max-ticks', dueRun[1]];
1864
+ if (dueRun[2]) args.push('--complete-on-pass');
1865
+ args.push('--json');
1866
+ return {
1867
+ action: 'mission_run_due',
1868
+ command: `atris ${args.join(' ')}`,
1869
+ args,
1870
+ heavy_work: true,
1871
+ setup_work: false,
1872
+ run_when_due_only: true,
1873
+ };
1874
+ }
1875
+
1876
+ return null;
1877
+ }
1878
+
1879
+ function shouldRunGoalLoopCommand(heartbeat, plan) {
1880
+ if (!heartbeat?.goal) return false;
1881
+ if (plan && plan.run_when_due_only === false) return true;
1882
+ return heartbeat.heartbeat?.due === true;
1883
+ }
1884
+
1885
+ function runMissionGoalNextCommand(root = process.cwd(), heartbeat, options = {}) {
1886
+ const plan = goalLoopNextCommandPlan(heartbeat?.goal);
1887
+ if (plan) {
1888
+ const args = [...plan.args];
1889
+ if (options.noClaude && plan.action === 'mission_run_due') args.push('--no-claude');
1890
+ return runAtrisMissionJsonCommand(root, args, {
1891
+ action: plan.action,
1892
+ heavyWork: plan.heavy_work,
1893
+ setupWork: plan.setup_work,
1894
+ });
1895
+ }
1896
+ return runMissionRunDueOnce(root, options);
1897
+ }
1898
+
1276
1899
  function sleep(ms, signal) {
1277
1900
  return new Promise((resolve, reject) => {
1278
1901
  if (ms <= 0) return resolve();
@@ -1600,7 +2223,20 @@ async function runMission(args) {
1600
2223
  const maxTicks = Math.max(1, Number(maxTicksFlag) || MISSION_RUN_DEFAULTS.maxTicks);
1601
2224
  const maxWallSeconds = Math.max(60, Number(readFlag(args, '--max-wall', '')) || MISSION_RUN_DEFAULTS.maxWallSeconds);
1602
2225
  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] || '';
2226
+ const ref = stripKnownFlags(
2227
+ args,
2228
+ ['--max-ticks', '--max-wall', '--cadence', '--owner', '--runner', '--lane', '--verify', '--stop', '--model'],
2229
+ ['--json', '--due', '--no-claude', '--no-verify', '--complete-on-pass', '--no-drain', '--always-on', '--xp-task', '--agent-xp'],
2230
+ ).join(' ').trim();
2231
+
2232
+ if (!dueMode && !ref) {
2233
+ if (asJson || !process.stdin.isTTY || !process.stderr.isTTY) {
2234
+ missionRunInputRequired(asJson);
2235
+ }
2236
+ const prompted = await promptMissionRunInput(args);
2237
+ startMissionFromRunObjective(prompted.objective, prompted.args);
2238
+ return;
2239
+ }
1604
2240
 
1605
2241
  let mission = dueMode && !ref ? selectDueMission() : resolveMission(ref);
1606
2242
  if (!mission && dueMode && !ref) {
@@ -1611,8 +2247,12 @@ async function runMission(args) {
1611
2247
  );
1612
2248
  return;
1613
2249
  }
2250
+ if (!mission && ref && /\s/.test(ref) && !String(ref).startsWith('mission-')) {
2251
+ startMissionFromRunObjective(ref, args);
2252
+ return;
2253
+ }
1614
2254
  if (!mission) {
1615
- exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
2255
+ exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
1616
2256
  }
1617
2257
  if (['complete', 'stopped'].includes(mission.status)) {
1618
2258
  if (asJson) {
@@ -1752,9 +2392,11 @@ async function runMission(args) {
1752
2392
  } else if (atris2Runner) {
1753
2393
  const prompt = buildTickPrompt(mission, tickIdx, effectiveMaxTicks, frozen);
1754
2394
  const { runAtris2Turn } = require('./probe');
2395
+ const businessId = businessIdForAtris2Mission(mission, cwd);
1755
2396
  const turn = await runAtris2Turn({
1756
2397
  prompt,
1757
2398
  model: mission.model || 'atris:fast',
2399
+ business: businessId,
1758
2400
  maxTurns: 16,
1759
2401
  signal: controller.signal,
1760
2402
  });
@@ -2201,22 +2843,43 @@ function completeMission(args) {
2201
2843
  exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
2202
2844
  }
2203
2845
  const baselineSummary = pruneMissionWorktreeBaseline(mission, process.cwd());
2204
- const next = {
2846
+ const completionGate = { ...gate, forced: force && !gate.ok };
2847
+ const baseNext = {
2205
2848
  ...mission,
2206
2849
  status: 'complete',
2207
2850
  completed_at: stampIso(),
2208
2851
  proof,
2209
- completion_gate: { ...gate, forced: force && !gate.ok },
2852
+ completion_gate: completionGate,
2210
2853
  worktree_baseline: baselineSummary || mission.worktree_baseline || null,
2211
2854
  next_action: 'mission complete',
2212
2855
  };
2856
+ const xpNextCommand = missionXpReadyAction(baseNext, proof);
2857
+ const completion = missionCompletionReceipt(baseNext, proof, xpNextCommand);
2858
+ const next = {
2859
+ ...baseNext,
2860
+ landing: completion.landing,
2861
+ result: completion.result,
2862
+ };
2213
2863
  const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof, completion_gate: next.completion_gate });
2214
2864
  const logPath = appendMemberLog(saved.owner, 'Mission completed', { mission: saved.objective, proof });
2215
2865
  const codexGoalState = refreshCodexGoalController(process.cwd());
2216
- const xpNextCommand = missionXpReadyAction(saved, proof);
2217
2866
  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}`] : [])],
2867
+ {
2868
+ ok: true,
2869
+ action: 'mission_completed',
2870
+ mission: saved,
2871
+ landing: completion.landing,
2872
+ result: completion.result,
2873
+ log_path: logPath,
2874
+ codex_goal_state: codexGoalState,
2875
+ xp_next_command: xpNextCommand,
2876
+ },
2877
+ [
2878
+ `Completed mission: ${saved.objective}`,
2879
+ ...missionResultLines(completion),
2880
+ `Proof: ${proof}`,
2881
+ ...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : []),
2882
+ ],
2220
2883
  asJson,
2221
2884
  );
2222
2885
  }
@@ -2311,16 +2974,27 @@ async function goalLoopMission(args) {
2311
2974
  iteration: index + 1,
2312
2975
  heartbeat,
2313
2976
  ran_heavy_work: false,
2977
+ ran_setup_work: false,
2314
2978
  dry_run: dryRun,
2315
2979
  };
2316
2980
 
2317
- if (heartbeat.heartbeat?.due && heartbeat.goal) {
2981
+ const commandPlan = goalLoopNextCommandPlan(heartbeat.goal);
2982
+ if (shouldRunGoalLoopCommand(heartbeat, commandPlan)) {
2318
2983
  if (dryRun) {
2319
2984
  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' };
2985
+ event.ran_setup_work = false;
2986
+ event.run = {
2987
+ skipped: true,
2988
+ reason: 'dry-run',
2989
+ action: commandPlan?.action || 'mission_run_due',
2990
+ command: commandPlan?.command || 'atris mission run --due --max-ticks 1 --complete-on-pass --json',
2991
+ heavy_work: commandPlan?.heavy_work === true,
2992
+ setup_work: commandPlan?.setup_work === true,
2993
+ };
2321
2994
  } else {
2322
- event.run = runMissionRunDueOnce(root, { noClaude });
2323
- event.ran_heavy_work = event.run.ok === true;
2995
+ event.run = runMissionGoalNextCommand(root, heartbeat, { noClaude });
2996
+ event.ran_heavy_work = event.run.ok === true && event.run.heavy_work === true;
2997
+ event.ran_setup_work = event.run.ok === true && event.run.setup_work === true;
2324
2998
  event.after_run = refreshCodexGoalController(root, { heartbeat: true });
2325
2999
  }
2326
3000
  }
@@ -2347,6 +3021,7 @@ async function goalLoopMission(args) {
2347
3021
  max_iterations: maxIterations,
2348
3022
  max_wall_seconds: maxWallSeconds,
2349
3023
  heavy_runs: events.filter((event) => event.ran_heavy_work).length,
3024
+ setup_runs: events.filter((event) => event.ran_setup_work).length,
2350
3025
  events,
2351
3026
  final_state: finalState,
2352
3027
  };
@@ -2373,15 +3048,17 @@ atris mission - durable goal + loop + owner + proof state
2373
3048
  default model atris:fast; runner codex_goal publishes the goal for a live
2374
3049
  Codex session to pull via atris mission goal)
2375
3050
  atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
3051
+ atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
3052
+ atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
2376
3053
  atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
2377
3054
  atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
2378
3055
  (rolls up sibling git-worktree missions; --local scopes to this checkout)
2379
3056
  atris mission goal [--heartbeat] [--json]
2380
3057
  atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
2381
3058
  atris mission tick <id> [--verify] [--complete-on-pass] [--summary "..."] [--json]
2382
- atris mission run <id|--due> [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
3059
+ atris mission run ["objective"|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
2383
3060
  [--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--json]
2384
- (always-on runs drain the review lane each tick; --no-drain skips)
3061
+ (bare run prompts for mission + owner; --due runs the saved queue)
2385
3062
  atris mission complete <id> --proof "..."
2386
3063
  atris mission stop <id> [--pause] [--reason "..."]
2387
3064
 
@@ -2389,7 +3066,7 @@ Autonomy recipe:
2389
3066
  1. Pick an owner member: atris member create <member> (if missing)
2390
3067
  2. Start a current-agent mission with a verifier:
2391
3068
  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
3069
+ 3. Codex sessions: atris mission goal --json, then mirror goal.visible_goal into the native chat goal
2393
3070
  Overnight controller: atris mission goal --heartbeat --json
2394
3071
  Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
2395
3072
  4. Do one bounded step, then record it:
@@ -2582,6 +3259,13 @@ function missionCommand(args) {
2582
3259
  case 'list':
2583
3260
  case 'ls':
2584
3261
  return statusMission(rest);
3262
+ case 'attach-task':
3263
+ case 'ensure-task':
3264
+ case 'task-spine':
3265
+ return attachMissionTask(rest);
3266
+ case 'report':
3267
+ case 'debrief':
3268
+ return reportMission(rest);
2585
3269
  case 'watch':
2586
3270
  return watchMission(rest);
2587
3271
  case 'layers':
@@ -2625,6 +3309,7 @@ module.exports = {
2625
3309
  classifyPathsByLayer,
2626
3310
  resolveClaudeRunnerModel,
2627
3311
  resolveClaudeRunnerBin,
3312
+ businessIdForAtris2Mission,
2628
3313
  detectUnavailableModel,
2629
3314
  missionPauseNextAction,
2630
3315
  consecutiveSameReasonErrors,