atris 3.52.0 → 3.55.0

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 (131) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/engines/SKILL.md +8 -7
  7. package/atris/skills/x-search/SKILL.md +24 -22
  8. package/atris/skills/youtube/SKILL.md +89 -40
  9. package/atris/team/_template/SOUL.md +40 -0
  10. package/atris/team/validator/MEMBER.md +1 -2
  11. package/ax +592 -91
  12. package/bin/atris +21 -0
  13. package/bin/atris.js +580 -378
  14. package/commands/api-key.js +170 -0
  15. package/commands/auth.js +453 -43
  16. package/commands/autoland.js +3 -1
  17. package/commands/autopilot-front.js +58 -11
  18. package/commands/avail.js +5 -4
  19. package/commands/balance.js +55 -0
  20. package/commands/bench.js +29 -1
  21. package/commands/brain.js +63 -12
  22. package/commands/brainstorm.js +18 -0
  23. package/commands/business.js +149 -1
  24. package/commands/chat-scan.js +17 -4
  25. package/commands/close.js +5 -1
  26. package/commands/computer.js +2 -2
  27. package/commands/doctor.js +174 -0
  28. package/commands/dream.js +7 -0
  29. package/commands/drill.js +34 -2
  30. package/commands/engine.js +47 -35
  31. package/commands/errors.js +58 -8
  32. package/commands/experiments.js +14 -0
  33. package/commands/feedback.js +61 -2
  34. package/commands/fleet-report.js +27 -7
  35. package/commands/fleet.js +98 -11
  36. package/commands/founder.js +60 -21
  37. package/commands/human-missions.js +79 -4
  38. package/commands/improve.js +54 -4
  39. package/commands/init.js +68 -92
  40. package/commands/install.js +40 -0
  41. package/commands/integrations.js +399 -30
  42. package/commands/interview.js +8 -0
  43. package/commands/land.js +48 -17
  44. package/commands/learn.js +29 -4
  45. package/commands/log.js +51 -0
  46. package/commands/member.js +68 -10
  47. package/commands/mission.js +339 -74
  48. package/commands/now.js +25 -6
  49. package/commands/orb.js +97 -2
  50. package/commands/pack.js +83 -9
  51. package/commands/playbook.js +381 -0
  52. package/commands/plugin.js +16 -0
  53. package/commands/radar.js +96 -40
  54. package/commands/recap.js +128 -21
  55. package/commands/release.js +102 -2
  56. package/commands/review.js +5 -10
  57. package/commands/revisions.js +55 -0
  58. package/commands/run-front.js +12 -4
  59. package/commands/scout.js +12 -1
  60. package/commands/search.js +123 -28
  61. package/commands/sign.js +2 -2
  62. package/commands/signup.js +12 -3
  63. package/commands/site-deploy.js +17 -4
  64. package/commands/site.js +2 -2
  65. package/commands/skill-eval.js +289 -0
  66. package/commands/skill.js +56 -6
  67. package/commands/social.js +603 -0
  68. package/commands/spaceship.js +69 -6
  69. package/commands/stream.js +8 -1
  70. package/commands/study.js +32 -19
  71. package/commands/sync.js +28 -22
  72. package/commands/task.js +418 -60
  73. package/commands/teach.js +72 -63
  74. package/commands/terminal.js +25 -9
  75. package/commands/topup.js +88 -0
  76. package/commands/truth.js +52 -6
  77. package/commands/usage.js +91 -0
  78. package/commands/verify.js +3 -2
  79. package/commands/version.js +3 -1
  80. package/commands/voice.js +5 -1
  81. package/commands/who.js +69 -11
  82. package/commands/wiki.js +7 -4
  83. package/commands/wish.js +76 -5
  84. package/commands/workflow.js +327 -145
  85. package/commands/worktree.js +1 -0
  86. package/commands/write.js +5 -0
  87. package/commands/x-search.js +340 -0
  88. package/commands/xp.js +5 -1
  89. package/commands/youtube.js +551 -33
  90. package/lib/account-bound.js +47 -0
  91. package/lib/auto-accept-certified.js +10 -2
  92. package/lib/auto-lessons.js +186 -0
  93. package/lib/autoland.js +4 -2
  94. package/lib/chat-log-scan.js +7 -4
  95. package/lib/cli-json.js +77 -0
  96. package/lib/cli-scope.js +26 -0
  97. package/lib/codex-flight.js +1 -0
  98. package/lib/conductor-artifacts.js +1 -1
  99. package/lib/context-gatherer.js +11 -0
  100. package/lib/developer-api.js +116 -0
  101. package/lib/engine-ask.js +11 -3
  102. package/lib/engine-registry.js +32 -7
  103. package/lib/feedback-triage.js +274 -0
  104. package/lib/first-minute.js +398 -0
  105. package/lib/fleet.js +115 -34
  106. package/lib/functional-owner.js +22 -0
  107. package/lib/known-commands.js +31 -5
  108. package/lib/member-scaffold.js +197 -0
  109. package/lib/mission-ledger-compact.js +127 -0
  110. package/lib/mission-root.js +4 -2
  111. package/lib/mission-runtime-loop.js +30 -2
  112. package/lib/next-moves.js +34 -34
  113. package/lib/noninteractive.js +88 -0
  114. package/lib/permission-grants.js +10 -1
  115. package/lib/policy-lessons.js +4 -1
  116. package/lib/revision-metric.js +279 -0
  117. package/lib/runner-command.js +5 -4
  118. package/lib/scratch-root.js +48 -0
  119. package/lib/skill-eval-gate.js +249 -0
  120. package/lib/task-db.js +22 -2
  121. package/lib/task-proof.js +75 -9
  122. package/lib/task-receipt.js +5 -1
  123. package/lib/verifier-quality.js +69 -0
  124. package/lib/wish-audit.js +2 -2
  125. package/lib/wish-delegate.js +10 -1
  126. package/lib/workspace-scaffold.js +270 -0
  127. package/package.json +4 -2
  128. package/scripts/det/checklist-score.js +191 -0
  129. package/scripts/det/ytsearch +31 -0
  130. package/scripts/outbound-artifact-gate.js +227 -0
  131. package/utils/update-check.js +15 -0
@@ -6,6 +6,11 @@ const crypto = require('crypto');
6
6
  const readline = require('readline');
7
7
  const { spawn, spawnSync } = require('child_process');
8
8
  const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
9
+ const {
10
+ compactErrorPayload,
11
+ compactSuccessPayload,
12
+ printCliJson,
13
+ } = require('../lib/cli-json');
9
14
  const escapeRegExp = require('../lib/escape-regexp');
10
15
  const {
11
16
  appendBriefRecord,
@@ -53,6 +58,7 @@ const {
53
58
  runsPruneLines,
54
59
  formatBytes,
55
60
  } = require('../lib/runs-prune');
61
+ const { compactMissionLedger } = require('../lib/mission-ledger-compact');
56
62
  const autolandLib = require('../lib/autoland');
57
63
  const { gateForHuman, landingWhyClause } = require('../lib/voice-gate');
58
64
  const { operatorReady, hasAgentJargon } = require('./autoland');
@@ -82,6 +88,7 @@ const {
82
88
  resolveDefaultVerifier,
83
89
  } = require('../lib/default-verifier');
84
90
  const { resolveWorkspaceRoot, redirectToWorkspaceRoot } = require('../lib/mission-root');
91
+ const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
85
92
  const { readJson, writeJson } = require('../lib/json-file');
86
93
  const {
87
94
  normalizeHumanAsks,
@@ -426,8 +433,22 @@ function applyMissionRunnerProfile(runner) {
426
433
  }
427
434
 
428
435
  function exitMissionError(message, code = 1, asJson = false) {
429
- if (asJson) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
430
- else console.error(message);
436
+ if (asJson) {
437
+ printCliJson(
438
+ {
439
+ ok: false,
440
+ error: message,
441
+ reason: 'mission_error',
442
+ detail: message,
443
+ selected_ref: null,
444
+ next_command: null,
445
+ },
446
+ compactErrorPayload({ reason: 'mission_error', detail: message }),
447
+ process.argv.slice(2),
448
+ );
449
+ } else {
450
+ console.error(message);
451
+ }
431
452
  process.exit(code);
432
453
  }
433
454
 
@@ -517,7 +538,7 @@ function assertMissionVerifier(command, asJson = false) {
517
538
 
518
539
  function stripKnownFlags(args, valueNames, booleanNames = []) {
519
540
  const valueSet = new Set(valueNames);
520
- const booleanSet = new Set(booleanNames);
541
+ const booleanSet = new Set(['--full', '--verbose', ...booleanNames]);
521
542
  const out = [];
522
543
  for (let i = 0; i < args.length; i += 1) {
523
544
  const arg = String(args[i]);
@@ -568,6 +589,8 @@ const MISSION_RUN_VALUE_FLAGS = [
568
589
  ];
569
590
  const MISSION_RUN_BOOLEAN_FLAGS = [
570
591
  '--json',
592
+ '--full',
593
+ '--verbose',
571
594
  '--due',
572
595
  '--detach',
573
596
  '--headless',
@@ -726,7 +749,72 @@ function missionXpTaskTitle(objective) {
726
749
  return `${prefix}${shortClause || 'mission work'}`;
727
750
  }
728
751
 
752
+ function normalizeMissionTaskMatchText(text) {
753
+ return String(text || '')
754
+ .replace(/^Mission XP:\s*/i, '')
755
+ .toLowerCase()
756
+ .replace(/\s+/g, ' ')
757
+ .trim();
758
+ }
759
+
760
+ function missionObjectiveTaskMatch(objective, title) {
761
+ const want = normalizeMissionTaskMatchText(objective);
762
+ const have = normalizeMissionTaskMatchText(title);
763
+ if (!want || !have) return false;
764
+ if (want === have) return true;
765
+ // First clause of a long objective often matches the claimed task title.
766
+ const wantClause = want.split(/:|[.!?](?=\s|$)/)[0].trim();
767
+ const haveClause = have.split(/:|[.!?](?=\s|$)/)[0].trim();
768
+ if (wantClause && haveClause && (wantClause === haveClause || want.startsWith(haveClause) || have.startsWith(wantClause))) {
769
+ return true;
770
+ }
771
+ return false;
772
+ }
773
+
774
+ function taskRowToMissionXpBinding(task, ownerResolution, outPath, { inserted = false, reused = true } = {}) {
775
+ return {
776
+ task_id: task.id,
777
+ ref: missionTaskRef(task) || task.id,
778
+ title: task.title,
779
+ operator_title_warning: warnIfTaskTitleNeedsOperatorWhy(task.title),
780
+ status: task.status || 'claimed',
781
+ assigned_to: task.claimed_by || ownerResolution.owner,
782
+ owner_resolution: ownerResolution.reason,
783
+ executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
784
+ inserted,
785
+ reused,
786
+ projection_path: outPath,
787
+ };
788
+ }
789
+
790
+ function findOpenTaskMatchingMissionObjective(mission, root = process.cwd(), asJson = false) {
791
+ const taskDb = loadTaskDb(asJson);
792
+ const db = taskDb.open();
793
+ const workspaceRoot = taskDb.workspaceRoot(root);
794
+ const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
795
+ const open = rows.filter((row) => row && ['open', 'claimed', 'review'].includes(row.status));
796
+ const explicitIds = Array.isArray(mission.task_ids) ? mission.task_ids.filter(Boolean) : [];
797
+ for (const id of explicitIds) {
798
+ const hit = open.find((row) => row.id === id || row.display_id === id || missionTaskRef(row) === id);
799
+ if (hit) return hit;
800
+ }
801
+ return open.find((row) => missionObjectiveTaskMatch(mission.objective, row.title)) || null;
802
+ }
803
+
804
+ function bindExistingMissionTask(mission, task, root = process.cwd(), asJson = false) {
805
+ const taskDb = loadTaskDb(asJson);
806
+ const db = taskDb.open();
807
+ const workspaceRoot = taskDb.workspaceRoot(root);
808
+ const ownerResolution = resolveMissionOwner(mission, workspaceRoot);
809
+ const { outPath } = writeMissionTaskProjection(taskDb, db, workspaceRoot);
810
+ return taskRowToMissionXpBinding(task, ownerResolution, outPath, { inserted: false, reused: true });
811
+ }
812
+
729
813
  function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
814
+ const existing = findOpenTaskMatchingMissionObjective(mission, root, asJson);
815
+ if (existing) {
816
+ return bindExistingMissionTask(mission, existing, root, asJson);
817
+ }
730
818
  const taskDb = loadTaskDb(asJson);
731
819
  const db = taskDb.open();
732
820
  const workspaceRoot = taskDb.workspaceRoot(root);
@@ -781,6 +869,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
781
869
  owner_resolution: ownerResolution.reason,
782
870
  executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
783
871
  inserted: result.inserted !== false,
872
+ reused: false,
784
873
  projection_path: outPath,
785
874
  };
786
875
  }
@@ -1237,11 +1326,27 @@ function exitMissingMission(ref, code = 1, asJson = false, root = process.cwd())
1237
1326
  } catch { /* best-effort hint only */ }
1238
1327
  }
1239
1328
  if (asJson) {
1240
- const payload = { ok: false, error };
1241
- if (hints.length) payload.workspace_hint = hints[0];
1242
- if (hints.length > 1) payload.workspace_hints = hints;
1243
- if (candidates.length) payload.nearest_candidates = candidates;
1244
- console.log(JSON.stringify(payload, null, 2));
1329
+ const nextCommand = hints[0] && hints[0].command ? hints[0].command : null;
1330
+ printCliJson(
1331
+ {
1332
+ ok: false,
1333
+ error,
1334
+ reason: 'mission_not_found',
1335
+ detail: error,
1336
+ selected_ref: ref || null,
1337
+ next_command: nextCommand,
1338
+ ...(hints.length ? { workspace_hint: hints[0] } : {}),
1339
+ ...(hints.length > 1 ? { workspace_hints: hints } : {}),
1340
+ ...(candidates.length ? { nearest_candidates: candidates } : {}),
1341
+ },
1342
+ compactErrorPayload({
1343
+ reason: 'mission_not_found',
1344
+ detail: error,
1345
+ selected_ref: ref || null,
1346
+ next_command: nextCommand,
1347
+ }),
1348
+ process.argv.slice(2),
1349
+ );
1245
1350
  } else {
1246
1351
  console.error(error);
1247
1352
  if (hints.length) {
@@ -1347,6 +1452,11 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
1347
1452
  updated_at: stampIso(),
1348
1453
  });
1349
1454
  appendJsonLine(paths.missionsJsonl, next);
1455
+ // Self-maintenance: once history outweighs live missions 4:1 the ledger is
1456
+ // rewritten to one row per mission (latest state, first display number —
1457
+ // exactly what loadMissionMap reconstructs). Every status/tick path parses
1458
+ // this file several times, so unbounded snapshot history taxes everything.
1459
+ try { compactMissionLedger(paths.missionsJsonl); } catch {}
1350
1460
  const event = appendEvent(eventType, next, payload, root);
1351
1461
  renderMissionStatus(root);
1352
1462
  renderMemberMissionState(next.owner, root);
@@ -1682,8 +1792,26 @@ function verifierTimeoutSeconds(verifierResult) {
1682
1792
  return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
1683
1793
  }
1684
1794
 
1685
- function missionVerifierCheckedText(verifierResult, mission) {
1686
- if (!verifierResult) return 'UNVERIFIED: tick recorded but nothing was checked; treat this increment as unproven.';
1795
+ function missionGuardBlockedText(guard) {
1796
+ if (!guard || guard.allowed !== false) return '';
1797
+ const detail = String(guard.detail || '').trim();
1798
+ const reason = String(guard.reason || 'the check was held').trim();
1799
+ return `VERIFY DID NOT RUN: ${reason}${detail ? ` (${detail})` : ''}. Treat this increment as unproven.`;
1800
+ }
1801
+
1802
+ function missionGuardNextText(guard, missionId) {
1803
+ if (!guard || guard.allowed !== false || !missionId) return '';
1804
+ if (guard.unreadable) {
1805
+ return `repair the mission diff tooling failure, then rerun: atris mission tick ${missionId} --verify`;
1806
+ }
1807
+ return `review the protected diff and receipt before resuming: atris mission run ${missionId}`;
1808
+ }
1809
+
1810
+ function missionVerifierCheckedText(verifierResult, mission, guard = null) {
1811
+ if (!verifierResult) {
1812
+ return missionGuardBlockedText(guard)
1813
+ || 'UNVERIFIED: tick recorded but nothing was checked; treat this increment as unproven.';
1814
+ }
1687
1815
  if (verifierResult.mode === 'engine-unavailable') {
1688
1816
  return 'VERIFY FAILED: the engine verify pass was unavailable; treat this increment as unproven.';
1689
1817
  }
@@ -1709,8 +1837,15 @@ function missionVerifierCheckedText(verifierResult, mission) {
1709
1837
  return `VERIFY FAILED: ${command}.`;
1710
1838
  }
1711
1839
 
1712
- function missionVerifierHighLevelTestText(verifierResult, mission) {
1713
- if (!verifierResult) return 'No automated verifier ran for this receipt; judge it from the receipt, changed files, and next action.';
1840
+ function missionVerifierHighLevelTestText(verifierResult, mission, guard = null) {
1841
+ if (!verifierResult) {
1842
+ if (guard && guard.allowed === false) {
1843
+ const detail = String(guard.detail || '').trim();
1844
+ const reason = String(guard.reason || 'the check was held').trim();
1845
+ return `No automated verifier ran: ${reason}${detail ? ` (${detail})` : ''}. Clear the hold, then rerun to get a real verdict.`;
1846
+ }
1847
+ return 'No automated verifier ran for this receipt; judge it from the receipt, changed files, and next action.';
1848
+ }
1714
1849
  if (verifierResult.mode === 'engine-unavailable') {
1715
1850
  return 'No changed-surface commands were confirmed because the engine verify pass was unavailable.';
1716
1851
  }
@@ -1761,11 +1896,12 @@ function missionFallbackChangedText(mission, status, tickIndex, { ranTicks = nul
1761
1896
  return `${mission.objective} recorded tick ${tickIndex}.`;
1762
1897
  }
1763
1898
 
1764
- function missionTickResultLines(mission, tickIndex, receiptPath, verifierResult = null, stepSummary = '') {
1899
+ function missionTickResultLines(mission, tickIndex, receiptPath, verifierResult = null, stepSummary = '', guard = null) {
1765
1900
  return missionLandingLines(missionReceiptLanding(mission, {
1766
- tick: { tick_index: tickIndex },
1901
+ tick: { tick_index: tickIndex, protected_lane_guard: guard },
1767
1902
  summary: stepSummary,
1768
1903
  verifier_result: verifierResult,
1904
+ protected_lane_guard: guard,
1769
1905
  }, receiptPath));
1770
1906
  }
1771
1907
 
@@ -1795,6 +1931,9 @@ function missionReceiptStatus(mission, result) {
1795
1931
  function missionReceiptNextText(mission, result, receiptPath = '') {
1796
1932
  if (result?.next) return String(result.next);
1797
1933
  if (result?.landing?.next) return String(result.landing.next);
1934
+ const guard = result?.protected_lane_guard || result?.tick?.protected_lane_guard || null;
1935
+ const guardNext = missionGuardNextText(guard, mission?.id);
1936
+ if (guardNext) return guardNext;
1798
1937
  if (result?.verifier_result?.passed === true) {
1799
1938
  if (!mission?.always_on && receiptPath) {
1800
1939
  return `Review proof, then run: atris mission complete ${mission.id} --proof "${receiptPath}".`;
@@ -1811,8 +1950,9 @@ function missionReceiptLanding(mission, result, receiptPath = '') {
1811
1950
  const summary = missionReceiptSummaryText(result);
1812
1951
  const changed = missionLandingStepSummary(summary)
1813
1952
  || missionFallbackChangedText(mission, status, missionReceiptTickIndex(mission, result));
1814
- const checked = missionVerifierCheckedText(verifierResult, mission);
1815
- const tested = missionVerifierHighLevelTestText(verifierResult, mission);
1953
+ const guard = result?.protected_lane_guard || result?.tick?.protected_lane_guard || null;
1954
+ const checked = missionVerifierCheckedText(verifierResult, mission, guard);
1955
+ const tested = missionVerifierHighLevelTestText(verifierResult, mission, guard);
1816
1956
  const reason = missionHumanReasonText(mission, changed);
1817
1957
  return {
1818
1958
  schema: 'atris.result_landing.v1',
@@ -2367,20 +2507,25 @@ function missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalRecei
2367
2507
  const changed = missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks, createdNext);
2368
2508
  const reason = missionHumanReasonText(mission, changed);
2369
2509
  const lastTick = ticks[ticks.length - 1] || null;
2510
+ const guard = lastTick?.protected_lane_guard || null;
2370
2511
  const verifier = lastTick && lastTick.verifier_passed == null ? null : mission.verifier_result;
2371
2512
  const checked = verifier
2372
- ? missionVerifierCheckedText(verifier, mission)
2373
- : pauseReason === 'no-progress'
2374
- ? `Run stopped: ${mission.stop_reason || 'no progress across consecutive ticks'}.`
2375
- : pauseReason
2376
- ? `Run paused: ${pauseReason}.`
2377
- : 'UNVERIFIED: run recorded but nothing was checked; treat this increment as unproven.';
2513
+ ? missionVerifierCheckedText(verifier, mission, guard)
2514
+ : missionGuardBlockedText(guard)
2515
+ || (pauseReason === 'no-progress'
2516
+ ? `Run stopped: ${mission.stop_reason || 'no progress across consecutive ticks'}.`
2517
+ : pauseReason
2518
+ ? `Run paused: ${pauseReason}.`
2519
+ : 'UNVERIFIED: run recorded but nothing was checked; treat this increment as unproven.');
2378
2520
  const tested = verifier
2379
- ? missionVerifierHighLevelTestText(verifier, mission)
2380
- : mission.verifier
2381
- ? `Verifier was configured but not completed: ${mission.verifier}.`
2382
- : 'No verifier command was recorded for this mission.';
2383
- const nextLine = missionRunCreatedNextLine(createdNext, continuationGoal, mission);
2521
+ ? missionVerifierHighLevelTestText(verifier, mission, guard)
2522
+ : (guard && guard.allowed === false)
2523
+ ? missionVerifierHighLevelTestText(null, mission, guard)
2524
+ : mission.verifier
2525
+ ? `Verifier was configured but not completed: ${mission.verifier}.`
2526
+ : 'No verifier command was recorded for this mission.';
2527
+ const nextLine = missionGuardNextText(guard, mission?.id)
2528
+ || missionRunCreatedNextLine(createdNext, continuationGoal, mission);
2384
2529
  const lines = [
2385
2530
  'Landing:',
2386
2531
  ` Changed: ${changed}`,
@@ -2654,6 +2799,7 @@ function missionFromArgs(args) {
2654
2799
  ...(ownerResolution.requested_owner && ownerResolution.requested_owner !== owner ? { requested_owner: ownerResolution.requested_owner } : {}),
2655
2800
  ...(ownerResolution.executed_by ? { executed_by: normalizeOwnerSlug(ownerResolution.executed_by) } : {}),
2656
2801
  ...(ownerResolution.proposed_member ? { proposed_member: ownerResolution.proposed_member } : {}),
2802
+ ...(ownerResolution.remap_reason ? { remap_reason: ownerResolution.remap_reason } : {}),
2657
2803
  status: 'planning',
2658
2804
  cadence,
2659
2805
  runner,
@@ -3965,9 +4111,23 @@ function startMission(args, options = {}) {
3965
4111
  const xpTask = createMissionXpTask(mission, process.cwd(), asJson);
3966
4112
  mission.xp_task = xpTask;
3967
4113
  mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
4114
+ mission.current_task_id = xpTask.task_id;
4115
+ mission.task_ref = xpTask.ref;
3968
4116
  if (!mission.verifier && !mission.always_on) {
3969
4117
  mission.next_action = `work task then run: atris task current-step --goal-id ${mission.id} --as ${mission.owner} --proof "<proof>" --json`;
3970
4118
  }
4119
+ } else if (!(mission.task_ids && mission.task_ids.length)) {
4120
+ // Bind a live open/claimed task with the same objective instead of leaving
4121
+ // task_ids empty and forcing attach-task to spawn a Mission XP twin later.
4122
+ const existing = findOpenTaskMatchingMissionObjective(mission, process.cwd(), asJson);
4123
+ if (existing) {
4124
+ const bound = bindExistingMissionTask(mission, existing, process.cwd(), asJson);
4125
+ mission.task_ids = [bound.task_id];
4126
+ mission.current_task_id = bound.task_id;
4127
+ mission.task_ref = bound.ref;
4128
+ mission.xp_task = bound;
4129
+ mission.xp_task_enabled = true;
4130
+ }
3971
4131
  }
3972
4132
  if (typeof options.beforeMissionSave === 'function') {
3973
4133
  mission = options.beforeMissionSave(mission) || mission;
@@ -4002,6 +4162,7 @@ function startMission(args, options = {}) {
4002
4162
  dirty_count: worktreeBaseline.dirty_count,
4003
4163
  dirty_hash: worktreeBaseline.dirty_hash,
4004
4164
  } : null,
4165
+ ...(saved.remap_reason ? { remap_reason: saved.remap_reason } : {}),
4005
4166
  };
4006
4167
  if (!options.silent) {
4007
4168
  printJsonOrText(
@@ -4010,6 +4171,7 @@ function startMission(args, options = {}) {
4010
4171
  `Started mission: ${saved.objective}`,
4011
4172
  `Owner: ${saved.owner}`,
4012
4173
  `State: ${saved.status}`,
4174
+ ...(saved.remap_reason ? [`Remap: ${saved.remap_reason}`] : []),
4013
4175
  ...(saved.worktree ? [`Worktree: ${saved.worktree.path}`, `Branch: ${saved.worktree.branch}`] : []),
4014
4176
  ...warnings.map((warning) => `Warning: ${warning.message}`),
4015
4177
  ...(saved.xp_task ? [`AgentXP task: ${saved.xp_task.ref}`] : []),
@@ -4130,6 +4292,8 @@ async function startMissionFromRunObjective(objective, args) {
4130
4292
  const xpTask = createMissionXpTask(mission, process.cwd(), asJson);
4131
4293
  mission.xp_task = xpTask;
4132
4294
  mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
4295
+ mission.current_task_id = xpTask.task_id;
4296
+ mission.task_ref = xpTask.ref;
4133
4297
  }
4134
4298
  if (landRun) {
4135
4299
  const repoPath = path.resolve(readFlag(args, '--repo', process.cwd()));
@@ -4256,14 +4420,16 @@ async function startMissionFromRunObjective(objective, args) {
4256
4420
  function attachMissionTask(args) {
4257
4421
  const asJson = wantsJson(args);
4258
4422
  if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
4259
- console.log('Usage: atris mission attach-task <id> [--json]');
4423
+ console.log('Usage: atris mission attach-task <id> [--task <task-id>] [--json]');
4424
+ console.log('Binds an open task with the same objective when one exists; pass --task to choose explicitly.');
4260
4425
  console.log('Run `atris mission --help` for the full option list.');
4261
4426
  process.exit(0);
4262
4427
  }
4263
- const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
4428
+ const ref = stripKnownFlags(args, ['--task'], ['--json'])[0] || '';
4264
4429
  if (!ref) {
4265
- exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
4430
+ exitMissionError('Usage: atris mission attach-task <id> [--task <task-id>] [--json]', 1, asJson);
4266
4431
  }
4432
+ const explicitTaskIds = readRepeatedFlag(args, '--task');
4267
4433
  let mission = resolveMission(ref);
4268
4434
  if (!mission) {
4269
4435
  exitMissingMission(ref, 1, asJson);
@@ -4281,6 +4447,13 @@ function attachMissionTask(args) {
4281
4447
  exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
4282
4448
  }
4283
4449
 
4450
+ if (explicitTaskIds.length) {
4451
+ mission = {
4452
+ ...mission,
4453
+ task_ids: Array.from(new Set([...(mission.task_ids || []), ...explicitTaskIds])),
4454
+ };
4455
+ }
4456
+
4284
4457
  const existingSpine = missionTaskSpine(mission);
4285
4458
  if (existingSpine?.has_task) {
4286
4459
  let currentMission = mission;
@@ -4356,6 +4529,8 @@ function attachMissionTask(args) {
4356
4529
  xp_task_enabled: true,
4357
4530
  xp_task: xpTask,
4358
4531
  task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
4532
+ current_task_id: xpTask.task_id,
4533
+ task_ref: xpTask.ref,
4359
4534
  };
4360
4535
  if (nextMission.status === 'ready' && nextMission.receipt_path) {
4361
4536
  nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
@@ -4367,17 +4542,17 @@ function attachMissionTask(args) {
4367
4542
  nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
4368
4543
  }
4369
4544
 
4370
- const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
4545
+ const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref, reused: xpTask.reused === true });
4371
4546
  const memberState = renderMemberMissionState(saved.owner);
4372
- const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
4547
+ const logPath = appendMemberLog(saved.owner, xpTask.reused ? 'Mission task spine bound to existing task' : 'Mission task spine attached', {
4373
4548
  mission: saved.objective,
4374
4549
  task: xpTask.ref,
4375
4550
  });
4376
4551
  const view = missionStatusView(saved);
4377
4552
  printJsonOrText(
4378
- { ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
4553
+ { ok: true, action: xpTask.reused ? 'mission_task_spine_bound' : 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
4379
4554
  [
4380
- `Attached task spine: ${saved.objective}`,
4555
+ xpTask.reused ? `Bound existing task spine: ${saved.objective}` : `Attached task spine: ${saved.objective}`,
4381
4556
  `Task: ${xpTask.ref}`,
4382
4557
  `Next: ${view.task_spine.current_step_command}`,
4383
4558
  ],
@@ -4810,27 +4985,60 @@ function missionTickVerification(tick) {
4810
4985
  return { verified: false, state: ran ? 'unchecked' : 'skipped', unchecked: ran };
4811
4986
  }
4812
4987
 
4813
- // Roll every ran tick for a mission into a verification-debt tally. `unchecked`
4814
- // counts ran ticks that recorded no verifier result at all — the "tick recorded
4815
- // but nothing was checked" red the report and now.md rollup surface and count
4816
- // against the mission.
4817
- function missionVerificationDebt(mission, root = process.cwd()) {
4818
- const paths = statePaths(root);
4988
+ // Per-process index of mission run receipts, keyed by runs dir. Reading and
4989
+ // parsing ~1400 receipt files per mission made `mission list` scale as
4990
+ // O(missions × runs-files); building the mission_id receipts map once per
4991
+ // invocation keeps outputs identical while scanning the dir a single time.
4992
+ // The dir mtime guards the memo so a receipt written mid-process (new file in
4993
+ // the dir) invalidates it; malformed receipts stay silently skipped, matching
4994
+ // readJson's existing tolerance.
4995
+ const missionRunsIndexCache = new Map();
4996
+
4997
+ function missionRunsReceiptIndex(root = process.cwd()) {
4998
+ const runsDir = statePaths(root).runsDir;
4999
+ let dirMtime = -1;
5000
+ try {
5001
+ dirMtime = fs.statSync(runsDir).mtimeMs;
5002
+ } catch {
5003
+ dirMtime = -1;
5004
+ }
5005
+ const cached = missionRunsIndexCache.get(runsDir);
5006
+ if (cached && cached.dirMtime === dirMtime) return cached.byMission;
5007
+
4819
5008
  let files = [];
4820
5009
  try {
4821
- files = fs.readdirSync(paths.runsDir)
5010
+ files = fs.readdirSync(runsDir)
4822
5011
  .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
4823
- .map((file) => path.join(paths.runsDir, file));
5012
+ .map((file) => path.join(runsDir, file));
4824
5013
  } catch {
4825
5014
  files = [];
4826
5015
  }
5016
+ const byMission = new Map();
5017
+ for (const file of files) {
5018
+ const receipt = readJson(file);
5019
+ if (!receipt || !receipt.mission_id) continue;
5020
+ let bucket = byMission.get(receipt.mission_id);
5021
+ if (!bucket) {
5022
+ bucket = [];
5023
+ byMission.set(receipt.mission_id, bucket);
5024
+ }
5025
+ bucket.push({ file, receipt });
5026
+ }
5027
+ missionRunsIndexCache.set(runsDir, { dirMtime, byMission });
5028
+ return byMission;
5029
+ }
5030
+
5031
+ // Roll every ran tick for a mission into a verification-debt tally. `unchecked`
5032
+ // counts ran ticks that recorded no verifier result at all — the "tick recorded
5033
+ // but nothing was checked" red the report and now.md rollup surface and count
5034
+ // against the mission.
5035
+ function missionVerificationDebt(mission, root = process.cwd()) {
5036
+ const entries = missionRunsReceiptIndex(root).get(mission.id) || [];
4827
5037
  let ran = 0;
4828
5038
  let verified = 0;
4829
5039
  let unchecked = 0;
4830
5040
  const seen = new Set();
4831
- for (const file of files) {
4832
- const receipt = readJson(file);
4833
- if (!receipt || receipt.mission_id !== mission.id) continue;
5041
+ for (const { receipt } of entries) {
4834
5042
  for (const tick of missionReceiptTicks(receipt)) {
4835
5043
  if (!tick || tick.status !== 'ran') continue;
4836
5044
  const key = `${tick.tick_index || ''}:${tick.finished_at || tick.started_at || ''}`;
@@ -4853,21 +5061,10 @@ function missionVerificationDebtLine(debt) {
4853
5061
  }
4854
5062
 
4855
5063
  function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
4856
- const paths = statePaths(root);
4857
- let files = [];
4858
- try {
4859
- files = fs.readdirSync(paths.runsDir)
4860
- .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
4861
- .map((file) => path.join(paths.runsDir, file));
4862
- } catch {
4863
- files = [];
4864
- }
4865
-
5064
+ const entries = missionRunsReceiptIndex(root).get(mission.id) || [];
4866
5065
  const items = [];
4867
5066
  const seen = new Set();
4868
- for (const file of files) {
4869
- const receipt = readJson(file);
4870
- if (!receipt || receipt.mission_id !== mission.id) continue;
5067
+ for (const { file, receipt } of entries) {
4871
5068
  const receiptPath = path.relative(root, file);
4872
5069
  for (const tick of missionReceiptTicks(receipt)) {
4873
5070
  const summary = missionTickReportSummary(tick);
@@ -5160,6 +5357,14 @@ function missionReportFor(mission, root = process.cwd()) {
5160
5357
  ? 'The last verifier passed; full-budget work is continuing.'
5161
5358
  : mission.operator_outcome
5162
5359
  || (verifierPassed ? 'Verifier passed.' : mission.status === 'complete' ? 'Mission is complete.' : mission.status === 'blocked' ? 'Mission is blocked.' : 'Mission is still in progress.');
5360
+ // Keep summary aligned with outcome: never claim a Codex handoff when the
5361
+ // same report also says no receipt / no live worker.
5362
+ let workerSummary = missionWorkerSummary(mission, receipt);
5363
+ if (awaitingFirstWorker || noLiveWorker) {
5364
+ workerSummary = awaitingFirstWorker
5365
+ ? 'Waiting for the first worker check-in; no receipt yet.'
5366
+ : 'No worker receipt yet.';
5367
+ }
5163
5368
  return {
5164
5369
  id: mission.id,
5165
5370
  objective: mission.objective,
@@ -5173,7 +5378,7 @@ function missionReportFor(mission, root = process.cwd()) {
5173
5378
  operator_outcome: operatorOutcome,
5174
5379
  verification: verificationDebt,
5175
5380
  worker: mission.worker || missionWorkerLabel(mission),
5176
- worker_summary: missionWorkerSummary(mission, receipt),
5381
+ worker_summary: workerSummary,
5177
5382
  timeline,
5178
5383
  worker_receipt_path: workerReceiptPath,
5179
5384
  verifier_receipt_path: verifierReceiptPath,
@@ -9500,6 +9705,7 @@ async function executeMissionRunTicksPhase(context) {
9500
9705
  tick: tickRecord,
9501
9706
  frozen,
9502
9707
  verifier_result: verifierResult,
9708
+ protected_lane_guard: result.protected_lane_guard || null,
9503
9709
  rate_limit_info: lastRateLimit,
9504
9710
  worktree: tickWorktree,
9505
9711
  });
@@ -9557,7 +9763,7 @@ async function executeMissionRunTicksPhase(context) {
9557
9763
  last_tick_index: tickIdx,
9558
9764
  last_tick_layer: result.layer,
9559
9765
  last_tick_layer_source: result.layer_source,
9560
- verifier_result: verifierResult || latestOnDisk.verifier_result || null,
9766
+ verifier_result: verifierResult || (verifyEach && result.protected_lane_guard && result.protected_lane_guard.allowed === false ? null : latestOnDisk.verifier_result) || null,
9561
9767
  last_check_feedback: verifierResult
9562
9768
  ? extractCheckFeedback(verifierResult)
9563
9769
  : latestOnDisk.last_check_feedback || '',
@@ -10068,9 +10274,7 @@ function tickMission(args) {
10068
10274
  const protectedLaneGuard = tickRecord.protected_lane_guard || { allowed: true };
10069
10275
  if (!protectedLaneGuard.allowed) {
10070
10276
  status = 'paused';
10071
- nextAction = protectedLaneGuard.unreadable
10072
- ? `repair the mission diff tooling failure, then rerun: atris mission run ${mission.id}`
10073
- : `review the protected diff and receipt before resuming: atris mission run ${mission.id}`;
10277
+ nextAction = missionGuardNextText(protectedLaneGuard, mission.id);
10074
10278
  } else if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
10075
10279
  status = 'running';
10076
10280
  nextAction = missionGoalChainNextAction(nextGoalChain);
@@ -10108,7 +10312,7 @@ function tickMission(args) {
10108
10312
  last_tick_index: tickIdx,
10109
10313
  last_tick_layer: tickRecord.layer,
10110
10314
  last_tick_layer_source: tickRecord.layer_source,
10111
- verifier_result: verifierResult || mission.verifier_result || null,
10315
+ verifier_result: verifierResult || (verify && !protectedLaneGuard.allowed ? null : mission.verifier_result) || null,
10112
10316
  last_check_feedback: verifierResult
10113
10317
  ? extractCheckFeedback(verifierResult)
10114
10318
  : mission.last_check_feedback || '',
@@ -10141,16 +10345,64 @@ function tickMission(args) {
10141
10345
  const outputMission = continuationGoal?.parent || saved;
10142
10346
  const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: outputMission.id });
10143
10347
  const codexGoalState = refreshCodexGoalController(process.cwd());
10144
- printJsonOrText(
10145
- { ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, blocker, receipt_path: receiptPath, log_path: logPath, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal, operator_summary_warning: operatorSummaryWarning, cached: Boolean(tickRecord.cached) },
10146
- [
10147
- ...(tickRecord.cached ? [`Reused receipt for tick ${tickIdx}: ${receiptPath}`] : []),
10148
- ...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary || tickRecord.summary),
10149
- ...(missionBlockerReceiptLine(blocker) ? [missionBlockerReceiptLine(blocker)] : []),
10150
- ...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
10151
- ],
10152
- asJson,
10153
- );
10348
+ const verifyToolingFailed = Boolean(verify && !verifierResult && protectedLaneGuard.unreadable);
10349
+ const skipDetail = missionGuardBlockedText(protectedLaneGuard)
10350
+ || 'VERIFY DID NOT RUN: the check could not start. Treat this increment as unproven.';
10351
+ const fullTick = {
10352
+ ok: !verifyToolingFailed,
10353
+ action: 'mission_tick',
10354
+ mission: outputMission,
10355
+ tick: tickRecord,
10356
+ verifier_result: verifierResult,
10357
+ blocker,
10358
+ receipt_path: receiptPath,
10359
+ log_path: logPath,
10360
+ atris_goal_state: atrisGoalState,
10361
+ codex_goal_state: codexGoalState,
10362
+ continuation_goal: continuationGoal,
10363
+ operator_summary_warning: operatorSummaryWarning,
10364
+ cached: Boolean(tickRecord.cached),
10365
+ next_command: outputMission.next_action || null,
10366
+ ...(verifyToolingFailed ? {
10367
+ reason: 'verify_did_not_run',
10368
+ detail: skipDetail,
10369
+ } : {}),
10370
+ };
10371
+ if (asJson) {
10372
+ printCliJson(
10373
+ fullTick,
10374
+ verifyToolingFailed
10375
+ ? compactErrorPayload({
10376
+ reason: 'verify_did_not_run',
10377
+ detail: skipDetail,
10378
+ selected_ref: outputMission.id,
10379
+ next_command: outputMission.next_action || null,
10380
+ })
10381
+ : compactSuccessPayload({
10382
+ action: 'mission_tick',
10383
+ ids: {
10384
+ mission_id: outputMission.id,
10385
+ tick_index: tickIdx,
10386
+ status: outputMission.status,
10387
+ receipt_path: receiptPath,
10388
+ },
10389
+ next_command: outputMission.next_action || null,
10390
+ }),
10391
+ args,
10392
+ );
10393
+ } else {
10394
+ printJsonOrText(
10395
+ fullTick,
10396
+ [
10397
+ ...(tickRecord.cached ? [`Reused receipt for tick ${tickIdx}: ${receiptPath}`] : []),
10398
+ ...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary || tickRecord.summary, protectedLaneGuard),
10399
+ ...(missionBlockerReceiptLine(blocker) ? [missionBlockerReceiptLine(blocker)] : []),
10400
+ ...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
10401
+ ],
10402
+ false,
10403
+ );
10404
+ }
10405
+ if (verifyToolingFailed) process.exitCode = 2;
10154
10406
  } finally {
10155
10407
  releaseMissionLock(lock);
10156
10408
  }
@@ -11121,6 +11373,18 @@ function missionCommand(args) {
11121
11373
  return require('./human-missions').answerCommand(args.slice(1));
11122
11374
  }
11123
11375
  if (isBareMission && !hasLocalMissionState(resolveWorkspaceRoot())) {
11376
+ const root = resolveWorkspaceRoot();
11377
+ // Empty folder talks like bare atris. After init, stay on the current
11378
+ // card or an honest no-mission, and do not bounce back to init.
11379
+ if (isFreshWorkspace(root)) {
11380
+ const code = speakFirstMinute({
11381
+ root,
11382
+ fresh: true,
11383
+ asJson: args.includes('--json'),
11384
+ });
11385
+ process.exitCode = code;
11386
+ return code;
11387
+ }
11124
11388
  return require('./human-missions').currentMissionCommand(args);
11125
11389
  }
11126
11390
  const subcommand = isBareMission ? 'status' : (args[0] || 'status');
@@ -11272,6 +11536,7 @@ module.exports = {
11272
11536
  missionVerifierTimeoutMs,
11273
11537
  missionLandingStepSummary,
11274
11538
  missionLandingLines,
11539
+ missionGuardNextText,
11275
11540
  missionVerifierCheckedText,
11276
11541
  missionVerifierHighLevelTestText,
11277
11542
  buildEngineVerifyPrompt,