atris 3.57.4 → 3.58.6

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 (175) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +55 -12
  6. package/atris/skills/x-search/SKILL.md +8 -6
  7. package/atris/skills/youtube/SKILL.md +66 -21
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +153 -23
  16. package/bin/atris.js +78 -30
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +59 -31
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +3 -3
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +216 -50
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +90 -82
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +101 -48
  98. package/commands/worktree.js +1 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +93 -22
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +1019 -123
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/auto-accept-certified.js +28 -12
  106. package/lib/autoland.js +6 -6
  107. package/lib/bench/engines.js +59 -8
  108. package/lib/bench/report.js +1 -1
  109. package/lib/bench/runner.js +204 -5
  110. package/lib/bench/tree-render.js +76 -0
  111. package/lib/card.js +1 -1
  112. package/lib/chat-log-scan.js +1 -1
  113. package/lib/claude-boot-block.js +2 -0
  114. package/lib/deck-compose.js +1 -1
  115. package/lib/deck-history.js +2 -2
  116. package/lib/deck-layout.js +1 -1
  117. package/lib/deck-review.js +5 -5
  118. package/lib/deck-schema.js +1 -1
  119. package/lib/default-verifier.js +1 -1
  120. package/lib/developer-api.js +3 -0
  121. package/lib/engine-ask.js +45 -7
  122. package/lib/engine-registry.js +15 -2
  123. package/lib/file-ops.js +1 -1
  124. package/lib/fleet.js +71 -35
  125. package/lib/html-render.js +1 -1
  126. package/lib/intents.js +172 -0
  127. package/lib/journal.js +1 -1
  128. package/lib/known-commands.js +2 -2
  129. package/lib/land-green.js +364 -0
  130. package/lib/lesson-contradiction.js +2 -2
  131. package/lib/lesson-preflight.js +1 -1
  132. package/lib/manifest.js +5 -5
  133. package/lib/member-alive.js +9 -5
  134. package/lib/memory-view.js +3 -3
  135. package/lib/mission-protected-lane.js +1 -1
  136. package/lib/mission-root.js +2 -2
  137. package/lib/orb-context.js +10 -10
  138. package/lib/pack-capabilities.js +861 -3
  139. package/lib/permission-grants.js +2 -2
  140. package/lib/policy-lessons.js +6 -6
  141. package/lib/pulse.js +8 -8
  142. package/lib/receipt-evidence.js +1 -1
  143. package/lib/reel.js +1 -1
  144. package/lib/review-integrity.js +1 -1
  145. package/lib/runner-command.js +35 -5
  146. package/lib/scorecard.js +4 -4
  147. package/lib/security-scan.js +5 -5
  148. package/lib/self-drive.js +1 -1
  149. package/lib/site-publish.js +372 -0
  150. package/lib/site.js +1 -1
  151. package/lib/slides-deck.js +2 -2
  152. package/lib/state-detection.js +2 -2
  153. package/lib/sync-telemetry.js +2 -2
  154. package/lib/task-db.js +17 -10
  155. package/lib/task-explanation.js +1 -1
  156. package/lib/task-proof.js +2 -2
  157. package/lib/task-receipt.js +3 -3
  158. package/lib/theme.js +12 -12
  159. package/lib/todo-fallback.js +3 -3
  160. package/lib/todo-sections.js +1 -1
  161. package/lib/tree-hash.js +79 -0
  162. package/lib/usage.js +1 -1
  163. package/lib/wish-delegate.js +1 -1
  164. package/lib/workspace-safety.js +4 -4
  165. package/lib/workspace-scaffold.js +3 -3
  166. package/package.json +1 -1
  167. package/scripts/det/concierge-bakeoff.sh +26 -0
  168. package/scripts/det/ytnotes +82 -5
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +47 -9
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
@@ -90,6 +90,7 @@ const {
90
90
  const { resolveWorkspaceRoot, redirectToWorkspaceRoot } = require('../lib/mission-root');
91
91
  const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
92
92
  const { readJson, writeJson } = require('../lib/json-file');
93
+ const { treeHashFor } = require('../lib/tree-hash');
93
94
  const {
94
95
  normalizeHumanAsks,
95
96
  openHumanAsks,
@@ -453,7 +454,7 @@ function exitMissionError(message, code = 1, asJson = false) {
453
454
  }
454
455
 
455
456
  // Write-time warnings judge only what a machine can truly judge: identifiers,
456
- // flags, and task ids. Whether a why is present is a judgment call the tick
457
+ // flags, and task ids. Whether a why is present is a judgment call, the tick
457
458
  // prompt demands it, the review pass judges it. A warning that cries wolf on
458
459
  // plain sentences teaches agents to ignore it (golden-path papercut). The
459
460
  // strict operatorReady bar stays at the digest surface, where under-showing
@@ -1203,7 +1204,7 @@ function listWorktreeRollupMissions(root = process.cwd()) {
1203
1204
  // run/tick/show. This is the same local+rollup, deduped-by-id, list-sorted
1204
1205
  // set `mission status`/`list` renders, so id/slug/suffix lookups search
1205
1206
  // exactly what list showed. n stays exactly as each mission's home workspace
1206
- // assigned it (loadMissionMap owns that) this index does NOT renumber,
1207
+ // assigned it (loadMissionMap owns that), this index does NOT renumber,
1207
1208
  // because n must stay a stable, durable handle across saves (see
1208
1209
  // mission-number-stability.test.js); numeric-n resolution stays scoped to
1209
1210
  // the local root, where n is guaranteed unique.
@@ -1266,7 +1267,7 @@ function missionNumberDisplayIndex(missions, canonicalMissions = missions) {
1266
1267
  const MISSION_REF_SUFFIX_MIN_LENGTH = 6;
1267
1268
 
1268
1269
  // A ref that "looks like" an id/suffix/number is a single token with no
1269
- // whitespace matching a hex/id/numeric shape. Those must resolve or error
1270
+ // whitespace matching a hex/id/numeric shape. Those must resolve or error,
1270
1271
  // never silently fall through to starting a new mission (a mistyped id/n
1271
1272
  // creating a junk mission is the worst failure mode here).
1272
1273
  function missionRefLooksLikeHandle(ref) {
@@ -1402,10 +1403,10 @@ function exitMissingMission(ref, code = 1, asJson = false, root = process.cwd())
1402
1403
  } else if (candidates.length) {
1403
1404
  console.error('Nearest candidates:');
1404
1405
  for (const candidate of candidates) {
1405
- console.error(` #${candidate.n} ${candidate.id} (${candidate.status}) ${candidate.objective}`);
1406
+ console.error(` #${candidate.n} ${candidate.id} (${candidate.status}), ${candidate.objective}`);
1406
1407
  }
1407
1408
  } else {
1408
- console.error('Mission ids are workspace-local run this from the workspace that created the mission.');
1409
+ console.error('Mission ids are workspace-local, run this from the workspace that created the mission.');
1409
1410
  }
1410
1411
  }
1411
1412
  process.exit(code);
@@ -1435,7 +1436,7 @@ function resolveMission(ref, root = process.cwd()) {
1435
1436
  return chosen;
1436
1437
  }
1437
1438
  if (matches.length) return matches[0];
1438
- // No mission has this n. An all-digit ref can still be a hash suffix
1439
+ // No mission has this n. An all-digit ref can still be a hash suffix,
1439
1440
  // missionId()'s 8-hex-char suffix is purely numeric ~2.4% of the time
1440
1441
  // ((10/16)^8), and returning null here made those missions unresolvable
1441
1442
  // by suffix. Fall through to slug/suffix resolution instead.
@@ -1500,7 +1501,7 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
1500
1501
  });
1501
1502
  appendJsonLine(paths.missionsJsonl, next);
1502
1503
  // Self-maintenance: once history outweighs live missions 4:1 the ledger is
1503
- // rewritten to one row per mission (latest state, first display number
1504
+ // rewritten to one row per mission (latest state, first display number,
1504
1505
  // exactly what loadMissionMap reconstructs). Every status/tick path parses
1505
1506
  // this file several times, so unbounded snapshot history taxes everything.
1506
1507
  try { compactMissionLedger(paths.missionsJsonl); } catch {}
@@ -1780,7 +1781,7 @@ function missionLandingStepSummary(summary) {
1780
1781
  const withoutProofTail = withoutInternalId.replace(/;\s+(?=(?:PR\s+\d+|(?:node|npm|git)\b)).*$/i, '').trim();
1781
1782
  const plainVerified = missionPlainVerifiedSummary(withoutProofTail);
1782
1783
  if (plainVerified) return plainVerified;
1783
- const beforeInlineProof = withoutProofTail.split(/\s+(?:—|-)\s+verified by\b/i)[0] || withoutProofTail;
1784
+ const beforeInlineProof = withoutProofTail.split(/\s+(?:\u2014|-)\s+verified by\b/i)[0] || withoutProofTail;
1784
1785
  const beforeChecks = (beforeInlineProof.split(/\s+(?:checks?|verified|proof):\s+/i)[0] || beforeInlineProof)
1785
1786
  .replace(/,\s*(?:verifier|proof|checks?)\s+(?:still|pending)[.!]?$/i, '')
1786
1787
  .trim();
@@ -2095,26 +2096,38 @@ function normalizeMissionReceiptResult(mission, result, receiptPath = '') {
2095
2096
 
2096
2097
  function missionRunStartNextLine(mission, nextCommand, warnings = []) {
2097
2098
  const missingVerifier = warnings.some((warning) => warning && warning.code === 'missing_verifier');
2098
- if (missingVerifier) return 'Add a verifier before completion, then run the first proof tick.';
2099
+ if (missingVerifier) return 'Add a completion check, then start the first piece of work.';
2099
2100
  if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
2100
- return 'Start the visible goal, then continue this mission.';
2101
+ return 'Start the first piece of work in this chat.';
2101
2102
  }
2102
- if (/attach-task/.test(nextCommand || '')) return 'Attach task context, then continue this mission.';
2103
- return 'Run the first proof tick.';
2103
+ if (/attach-task/.test(nextCommand || '')) return 'Start the first piece of work.';
2104
+ return 'Keep working on the next useful result.';
2105
+ }
2106
+
2107
+ function missionRunCheckPlanText(command) {
2108
+ const check = String(command || '').trim();
2109
+ if (!check) return 'No completion check is ready yet.';
2110
+ if (/^git\s+diff\s+--check\b/i.test(check)) {
2111
+ return 'Atris will check the changed files for formatting problems before the result is ready.';
2112
+ }
2113
+ if (/\bnode\s+--test\b/i.test(check)) {
2114
+ return 'Atris will run the relevant behavior checks before the result is ready.';
2115
+ }
2116
+ if (/^test\s+-s\s+\S+/i.test(check)) {
2117
+ return 'Atris will confirm the saved result exists and is not empty.';
2118
+ }
2119
+ return 'A completion check is ready and will run before the result is ready.';
2104
2120
  }
2105
2121
 
2106
2122
  function missionRunTakeoffLines(mission, { warnings = [], nextCommand = '' } = {}) {
2107
- const checked = mission.verifier
2108
- ? `Verifier configured: ${mission.verifier}.`
2109
- : 'No verifier was recorded for this mission.';
2110
2123
  return [
2111
- 'Takeoff:',
2124
+ 'Mission started.',
2112
2125
  ` Goal: ${mission.objective}`,
2113
- ` Done when: ${mission.stop_condition || 'the mission has proof or a human decision'}.`,
2126
+ ` Done when: ${mission.stop_condition || 'the result is checked and ready to review'}.`,
2114
2127
  ...(missionBudgetLine(mission) ? [` Budget: ${missionBudgetLine(mission)}`] : []),
2115
2128
  ...missionGoalChainLines(mission),
2116
- ' Proof: Mission state saved in .atris/state/missions.jsonl.',
2117
- ` Check: ${checked}`,
2129
+ ' Saved: Atris will remember this mission and its progress.',
2130
+ ` How it will be checked: ${missionRunCheckPlanText(mission.verifier)}`,
2118
2131
  ` Next: ${missionRunStartNextLine(mission, nextCommand, warnings)}`,
2119
2132
  ];
2120
2133
  }
@@ -2176,16 +2189,9 @@ function selectMissionRunUsefulTarget(rawObjective, root = process.cwd()) {
2176
2189
  }
2177
2190
  }
2178
2191
 
2179
- function missionRunPreflightSentence(text, max = 140) {
2180
- const clean = String(text || '').replace(/\s+/g, ' ').trim();
2181
- if (clean.length <= max) return clean;
2182
- return `${clean.slice(0, max - 3).replace(/\s+\S*$/, '').trimEnd()}...`;
2183
- }
2184
-
2185
- function missionRunPreflightObjective(rawObjective, room, owner) {
2186
- const name = room?.name || 'Mission Room';
2192
+ function missionRunPreflightObjective(rawObjective, room) {
2187
2193
  const task = room?.task_plan_preview?.task || room?.truth_snapshot || rawObjective;
2188
- return `${name} with ${owner}: turn "${missionRunPreflightSentence(task)}" into one visible goal, task spine, proof receipt, and next action.`;
2194
+ return String(task || rawObjective || '').replace(/\s+/g, ' ').trim();
2189
2195
  }
2190
2196
 
2191
2197
  function missionRunTrustedObjective(rawObjective, room, target) {
@@ -2257,7 +2263,7 @@ function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
2257
2263
  const written = writeMissionRoomReceipt(room, { root });
2258
2264
  const shapedObjective = trustedRun
2259
2265
  ? missionRunTrustedObjective(rawObjective, written.room, selectedTarget)
2260
- : missionRunPreflightObjective(rawObjective, written.room, ownerResolution.owner);
2266
+ : missionRunPreflightObjective(rawObjective, written.room);
2261
2267
  const shaping = guardMissionRunShaping(rawObjective, shapedObjective, selectedTarget);
2262
2268
  const acceptedTarget = shaping.selectedTarget;
2263
2269
  const taskSpineRequired = !acceptedTarget && (explicitPreflight || signalPreflight);
@@ -4022,7 +4028,7 @@ function inheritedWorktreeBase(cwd) {
4022
4028
  // Dedup gate: the same objective + owner already active anywhere in the
4023
4029
  // workspace family (this store or any worktree's) is reused, never cloned.
4024
4030
  // Born 2026-07-02: an hourly alive loop spawned six identical auto-improver
4025
- // missions in six fresh worktrees in one day pure token burn. --duplicate
4031
+ // missions in six fresh worktrees in one day, pure token burn. --duplicate
4026
4032
  // is the explicit escape hatch.
4027
4033
  const TWIN_ACTIVE_STATUSES = new Set(['planning', 'ready', 'running']);
4028
4034
 
@@ -4069,7 +4075,7 @@ function startMission(args, options = {}) {
4069
4075
  printJsonOrText(
4070
4076
  { ok: true, action: 'mission_recovered', recovered: true, mission: existing, note: 'objective looked like a mission id; recovered the existing mission instead of creating a new one' },
4071
4077
  [
4072
- `That's a mission id, not an objective recovered ${existing.id} (${existing.status}).`,
4078
+ `That's a mission id, not an objective, recovered ${existing.id} (${existing.status}).`,
4073
4079
  `Resume: atris mission run ${existing.id}`,
4074
4080
  ],
4075
4081
  asJson,
@@ -4119,7 +4125,7 @@ function startMission(args, options = {}) {
4119
4125
  { ok: true, action: 'mission_reused', reused: true, mission: twin, note: 'an active mission with this objective and owner already exists; resumed instead of cloning (pass --duplicate to force a second one)' },
4120
4126
  [
4121
4127
  `Already active: ${twin.id} (${twin.status})`,
4122
- `Same objective, same owner reusing it instead of starting a clone.`,
4128
+ `Same objective, same owner, reusing it instead of starting a clone.`,
4123
4129
  `Resume: atris mission run ${twin.id}`,
4124
4130
  `Really want a second one: re-run with --duplicate`,
4125
4131
  ],
@@ -4140,7 +4146,7 @@ function startMission(args, options = {}) {
4140
4146
  }
4141
4147
  // --worktree: bind the mission to its own isolated checkout. We chdir before
4142
4148
  // any state writes so the mission record, baseline sidecar, receipts, and
4143
- // member files all land inside the worktree ticks run there, and the main
4149
+ // member files all land inside the worktree, ticks run there, and the main
4144
4150
  // checkout's dirt never reaches the mission baseline.
4145
4151
  if (hasFlag(args, '--worktree')) {
4146
4152
  let created;
@@ -4268,14 +4274,15 @@ async function startMissionFromRunObjective(objective, args) {
4268
4274
  landRun ? DEFAULT_LONG_RUN_VERIFIER : inferRunObjectiveVerifier(missionObjective)
4269
4275
  || inferRunObjectiveVerifier(rawObjective)
4270
4276
  || (missionRunPreflight?.trusted_run ? DEFAULT_LONG_RUN_VERIFIER : '')
4271
- || (inferredLoop.wantsLongRun ? DEFAULT_LONG_RUN_VERIFIER : ''),
4277
+ || (inferredLoop.wantsLongRun ? DEFAULT_LONG_RUN_VERIFIER : '')
4278
+ || (hasFlag(args, '--no-verify') ? '' : resolveDefaultVerifier(process.cwd(), { allowBroadSuite: false })),
4272
4279
  );
4273
4280
  const stopCondition = readFlag(
4274
4281
  args,
4275
4282
  '--stop',
4276
4283
  budgetStopCondition(budgetContract) || (inferredLoop.wantsLongRun
4277
4284
  ? `run for ${inferredLoop.requestedHours || 'the requested overnight window'} hour${inferredLoop.requestedHours === 1 ? '' : 's'}, or stop when proof is ready`
4278
- : (verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready')),
4285
+ : (verifier ? 'the first result is checked and ready to review' : 'the first result is saved and ready to review')),
4279
4286
  );
4280
4287
  const startArgs = [
4281
4288
  missionObjective,
@@ -5083,7 +5090,7 @@ function missionRunsReceiptIndex(root = process.cwd()) {
5083
5090
  }
5084
5091
 
5085
5092
  // Roll every ran tick for a mission into a verification-debt tally. `unchecked`
5086
- // counts ran ticks that recorded no verifier result at all the "tick recorded
5093
+ // counts ran ticks that recorded no verifier result at all, the "tick recorded
5087
5094
  // but nothing was checked" red the report and now.md rollup surface and count
5088
5095
  // against the mission.
5089
5096
  function missionVerificationDebt(mission, root = process.cwd()) {
@@ -5111,7 +5118,7 @@ function missionVerificationDebt(mission, root = process.cwd()) {
5111
5118
  // when every ran tick was checked, so clean missions stay quiet.
5112
5119
  function missionVerificationDebtLine(debt) {
5113
5120
  if (!debt || !debt.unchecked) return '';
5114
- return `unverified ticks: ${debt.unchecked} of ${debt.ran} ran no check treat those increments as unproven`;
5121
+ return `unverified ticks: ${debt.unchecked} of ${debt.ran} ran no check ⚠, treat those increments as unproven`;
5115
5122
  }
5116
5123
 
5117
5124
  function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
@@ -6026,7 +6033,7 @@ function doctorMission(args) {
6026
6033
  if (findings.length) process.exitCode = 1;
6027
6034
  }
6028
6035
 
6029
- // `atris mission watch [id]` read-only live heartbeat. Prints a line per tick as it
6036
+ // `atris mission watch [id]`, read-only live heartbeat. Prints a line per tick as it
6030
6037
  // lands so a human (or any terminal) can see the loop is alive without rerunning status.
6031
6038
  function watchMission(args) {
6032
6039
  if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
@@ -6052,7 +6059,7 @@ function watchMission(args) {
6052
6059
  const fingerprint = (mission) => [mission.status, mission.last_tick_at, mission.last_tick_index, mission.receipt_path].join('|');
6053
6060
  const tickNote = (mission) => {
6054
6061
  const heartbeat = missionHeartbeatLines(mission).map((line) => line.trim()).join(', ');
6055
- return `${heartbeat || `state: ${mission.status}`}${mission.receipt_path ? ` proof: ${mission.receipt_path}` : ''}`;
6062
+ return `${heartbeat || `state: ${mission.status}`}${mission.receipt_path ? `, proof: ${mission.receipt_path}` : ''}`;
6056
6063
  };
6057
6064
  const seen = new Map();
6058
6065
  let lastIdleAt = Date.now();
@@ -6060,7 +6067,7 @@ function watchMission(args) {
6060
6067
  const poll = () => {
6061
6068
  const targets = loadTargets();
6062
6069
  if (!targets.length && !seen.size) {
6063
- emitOnce('no active missions yet waiting');
6070
+ emitOnce('no active missions yet, waiting');
6064
6071
  }
6065
6072
  let changed = false;
6066
6073
  for (const mission of targets) {
@@ -6101,6 +6108,7 @@ function writeReceipt(mission, result, root = process.cwd()) {
6101
6108
  const finalResult = normalizeMissionReceiptResult(mission, result, relativeReceiptPath);
6102
6109
  const receipt = {
6103
6110
  schema: 'atris.mission_receipt.v1',
6111
+ tree_hash: treeHashFor(root),
6104
6112
  mission_id: mission.id,
6105
6113
  objective: mission.objective,
6106
6114
  owner: mission.owner,
@@ -6349,7 +6357,7 @@ function loadMissionWorktreeBaseline(missionId, root = process.cwd()) {
6349
6357
  // Closed missions no longer tick, so the sidecar is dead weight; prune it and
6350
6358
  // fold a compact audit summary into the mission record (full path lists stay
6351
6359
  // out of missions.jsonl, which re-appends the whole record on every save).
6352
- // Paused missions keep their sidecar resume ticks still subtract it.
6360
+ // Paused missions keep their sidecar, resume ticks still subtract it.
6353
6361
  function pruneMissionWorktreeBaseline(mission, root = process.cwd()) {
6354
6362
  const baseline = loadMissionWorktreeBaseline(mission.id, root);
6355
6363
  try { fs.rmSync(missionBaselinePath(mission.id, root), { force: true }); } catch {}
@@ -6380,7 +6388,7 @@ function worktreeReceipt(before, after, { verifier = '', baseline = null } = {})
6380
6388
  // tick-start snapshot for legacy missions). Only paths dirtied beyond that
6381
6389
  // baseline count toward the unverified signal. Loop exhaust the mission
6382
6390
  // writes about itself (state plane, receipts, rendered status) is not work
6383
- // product, so it never counts otherwise every multi-tick mission in a repo
6391
+ // product, so it never counts, otherwise every multi-tick mission in a repo
6384
6392
  // that doesn't gitignore those dirs would flag its own bookkeeping.
6385
6393
  const baselinePaths = baseline
6386
6394
  ? new Set(baseline.paths)
@@ -6409,7 +6417,7 @@ function worktreeReceipt(before, after, { verifier = '', baseline = null } = {})
6409
6417
  }
6410
6418
 
6411
6419
  // ---------------------------------------------------------------------------
6412
- // `atris mission run <id>` bounded local headless loop. v0.1.
6420
+ // `atris mission run <id>`, bounded local headless loop. v0.1.
6413
6421
  // Spawns `claude -p --resume <session>` per tick. Honors cadence, active-hours,
6414
6422
  // rate-limit info, and a flock per mission. `max-ticks` bounds total attempts;
6415
6423
  // `ran_ticks` separately reports ticks that actually made progress.
@@ -7811,19 +7819,19 @@ function consecutiveVerifierFails(ticks) {
7811
7819
  }
7812
7820
 
7813
7821
  // BCK-1324: a "holding tick" is a tick that reports status=ran/reason=tick-ok
7814
- // (so it never trips the error-streak breakers) but left no structural trace
7822
+ // (so it never trips the error-streak breakers) but left no structural trace,
7815
7823
  // no new or cleared dirty files beyond whatever was already dirty at tick
7816
7824
  // start, and no fresh verifier pass. Claude's own summary text ("holding
7817
7825
  // tick, no drift") is not the signal: agents self-label busywork as progress
7818
7826
  // constantly. The worktree diff and verifier result are ground truth.
7819
7827
  function tickMadeProgress(tick) {
7820
- if (!tick || tick.status !== 'ran') return true; // errors/skips aren't "idle" other breakers own those
7828
+ if (!tick || tick.status !== 'ran') return true; // errors/skips aren't "idle", other breakers own those
7821
7829
  const wt = tick.worktree;
7822
7830
  if (wt && wt.available) {
7823
7831
  if ((wt.new_dirty_count || 0) > 0) return true;
7824
7832
  if ((wt.cleared_dirty_count || 0) > 0) return true;
7825
7833
  } else {
7826
- // No worktree signal available (e.g. git unavailable) don't punish a
7834
+ // No worktree signal available (e.g. git unavailable), don't punish a
7827
7835
  // tick we have no evidence against.
7828
7836
  return true;
7829
7837
  }
@@ -7833,7 +7841,7 @@ function tickMadeProgress(tick) {
7833
7841
 
7834
7842
  // Count the trailing run of ticks (most recent first) that made no progress
7835
7843
  // per tickMadeProgress. A single progressing tick anywhere in the run resets
7836
- // this to 0 only the tail streak matters.
7844
+ // this to 0, only the tail streak matters.
7837
7845
  function consecutiveNoProgressTicks(ticks) {
7838
7846
  let n = 0;
7839
7847
  for (let i = ticks.length - 1; i >= 0; i--) {
@@ -7854,7 +7862,7 @@ function missionJudgmentPlainText(value) {
7854
7862
  .replace(/\b[0-9A-HJKMNP-TV-Z]{26}\b/gi, ' ')
7855
7863
  .replace(/\b[0-9a-f]{12,40}\b/gi, ' ')
7856
7864
  .replace(/\b[A-Z][A-Z0-9]{1,9}-\d+\b/g, ' ')
7857
- .replace(/[—–]/g, ' ')
7865
+ .replace(/[\u2014–]/g, ' ')
7858
7866
  .replace(/[*_~#>]+/g, ' ')
7859
7867
  .replace(/\s+/g, ' ')
7860
7868
  .replace(/^[\s:;,.-]+|[\s:;,.-]+$/g, '')
@@ -8202,7 +8210,7 @@ function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
8202
8210
  const holderKnown = Number.isInteger(holderPid) && holderPid > 0;
8203
8211
  // A lock is created empty (openSync 'wx') and its pid record is written a
8204
8212
  // moment later. A racing waiter that reads the file inside that window sees
8205
- // no pid do NOT treat that as a dead owner, or two processes both "own"
8213
+ // no pid, do NOT treat that as a dead owner, or two processes both "own"
8206
8214
  // the lock and clobber each other's writes. Only steal when the holder pid
8207
8215
  // is known-dead, or when a pidless lock has sat abandoned past the tiny
8208
8216
  // creation window (guards against a crash between openSync and writeSync).
@@ -8434,7 +8442,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
8434
8442
  `**Lane:** ${frozen.lane}`,
8435
8443
  `**Cadence:** ${mission.cadence}`,
8436
8444
  `**Stop condition:** ${mission.stop_condition || 'human marks complete'}`,
8437
- `**Verifier (frozen):** ${frozen.verifier || '(none receipt only)'}`,
8445
+ `**Verifier (frozen):** ${frozen.verifier || '(none, receipt only)'}`,
8438
8446
  `**Last status:** ${mission.status}`,
8439
8447
  `**Last tick:** ${mission.last_tick_at || 'never'}`,
8440
8448
  ...missionBudgetPromptLines(mission),
@@ -8442,7 +8450,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
8442
8450
  ``,
8443
8451
  `## Your task`,
8444
8452
  `Do ONE increment of work toward the stop condition. ONE. No more.`,
8445
- `- You are the member "${mission.owner}". Read atris/team/${mission.owner}/MEMBER.md (and SOUL.md if present) before acting work in that identity, inside its scope and stop rules. After your work, append what you did and what you learned to atris/team/${mission.owner}/logs/<today's date>.md.`,
8453
+ `- You are the member "${mission.owner}". Read atris/team/${mission.owner}/MEMBER.md (and SOUL.md if present) before acting, work in that identity, inside its scope and stop rules. After your work, append what you did and what you learned to atris/team/${mission.owner}/logs/<today's date>.md.`,
8446
8454
  `- FIRST: inspect current mission/task state before acting. Read the relevant files, run \`atris mission status ${mission.id}\`, \`git status\`, or \`atris task list\` as needed so you know what's already done.`,
8447
8455
  `- Pick the smallest concrete action that moves the mission forward.`,
8448
8456
  `- Before acting, state your single next move in one sentence.`,
@@ -8473,14 +8481,14 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
8473
8481
  `- Do NOT start new missions, modify other missions, or expand scope.`,
8474
8482
  `- Do NOT run destructive commands without strong evidence they're correct.`,
8475
8483
  ``,
8476
- `When done, output a short receipt. The summary's first line must name what changed and how it was verified. Then include: (1) the exact files edited / commands run / artifacts produced name them, (1b) one verify command a reviewer can rerun to check the work, (2) the metric of progress, (3) what the next tick should pick up. End the receipt with one line naming the layer this tick touched: \`layer: identity|beliefs|capabilities|behaviors|environment\` (final line the harness parses it).`,
8484
+ `When done, output a short receipt. The summary's first line must name what changed and how it was verified. Then include: (1) the exact files edited / commands run / artifacts produced, name them, (1b) one verify command a reviewer can rerun to check the work, (2) the metric of progress, (3) what the next tick should pick up. End the receipt with one line naming the layer this tick touched: \`layer: identity|beliefs|capabilities|behaviors|environment\` (final line, the harness parses it).`,
8477
8485
  ];
8478
8486
  if (mission.task_ids?.length) {
8479
8487
  lines.push('', `## Task ids`, mission.task_ids.map((t) => `- ${t}`).join('\n'));
8480
8488
  }
8481
8489
  const humanAsks = openHumanAsks(mission.human_asks);
8482
8490
  if (humanAsks.length) {
8483
- lines.push('', `## Human asks (don't act on these surface them)`, humanAsks.map((ask) => `- ${ask.text}`).join('\n'));
8491
+ lines.push('', `## Human asks (don't act on these, surface them)`, humanAsks.map((ask) => `- ${ask.text}`).join('\n'));
8484
8492
  }
8485
8493
  return lines.join('\n');
8486
8494
  }
@@ -8515,16 +8523,16 @@ function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastEr
8515
8523
  return 'stopped: the worker kept reporting the same thing 3 times in a row';
8516
8524
  }
8517
8525
  if (pauseReason === 'model-unavailable' && deadModel) {
8518
- return `model "${deadModel}" is unavailable set a live model (mission.model, ATRIS_RUNNER_MODEL, or legacy ATRIS_CLAUDE_MODEL), then: atris mission run ${missionId}`;
8526
+ return `model "${deadModel}" is unavailable, set a live model (mission.model, ATRIS_RUNNER_MODEL, or legacy ATRIS_CLAUDE_MODEL), then: atris mission run ${missionId}`;
8519
8527
  }
8520
8528
  if (typeof pauseReason === 'string' && pauseReason.startsWith('repeated-error:')) {
8521
8529
  const reason = pauseReason.slice('repeated-error:'.length);
8522
- return `tick kept failing with "${reason}" inspect the last receipt, fix the cause, then: atris mission run ${missionId}`;
8530
+ return `tick kept failing with "${reason}", inspect the last receipt, fix the cause, then: atris mission run ${missionId}`;
8523
8531
  }
8524
8532
  // Single-tick cron runs pause via max-ticks-reached on the very first errored tick.
8525
8533
  // A bare "resume" there just re-errors; point the operator at the cause instead.
8526
8534
  if (pauseReason === 'max-ticks-reached' && lastErrorReason) {
8527
- return `hit the tick budget while erroring ("${lastErrorReason}") inspect the last receipt before resuming: atris mission run ${missionId}`;
8535
+ return `hit the tick budget while erroring ("${lastErrorReason}"), inspect the last receipt before resuming: atris mission run ${missionId}`;
8528
8536
  }
8529
8537
  return `resume with: atris mission run ${missionId}`;
8530
8538
  }
@@ -9081,7 +9089,7 @@ async function parseAndValidateMissionRunPhase(args) {
9081
9089
  }
9082
9090
  // --fleet: staff every idle capable engine on the board's claimable
9083
9091
  // safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
9084
- // full loop see lib/fleet.js. --dry-run previews the staffing only.
9092
+ // full loop, see lib/fleet.js. --dry-run previews the staffing only.
9085
9093
  if (hasFlag(args, '--fleet')) {
9086
9094
  const { runFleetFlight } = require('../lib/fleet');
9087
9095
  const slots = Math.max(1, Number(readFlag(args, '--slots', '')) || 3);
@@ -9202,7 +9210,7 @@ async function resolveMissionRunPhase(context) {
9202
9210
  // BCK-1319: a bare single token that looks like an id/suffix/number (no
9203
9211
  // whitespace, hex/id/numeric shape) is a mistyped or stale mission handle,
9204
9212
  // not a new objective. Silently starting a fresh mission from it is the
9205
- // worst failure mode it buries the mission the operator meant to run.
9213
+ // worst failure mode, it buries the mission the operator meant to run.
9206
9214
  // Only a genuine multi-word (or non-handle-shaped) ref keeps the
9207
9215
  // start-a-new-mission shortcut.
9208
9216
  if (!mission && ref && missionRefLooksLikeHandle(ref)) {
@@ -9383,7 +9391,7 @@ async function executeMissionRunTicksPhase(context) {
9383
9391
  const effectiveMaxTicks = (cadenceSeconds === 0 && !hasExplicitTickBudget) ? 1 : maxTicks;
9384
9392
 
9385
9393
  // Session setup: only Claude-backed workers need a persisted session id.
9386
- // atris2 turns are stateless per tick continuity lives on disk (logs, receipts, now.md).
9394
+ // atris2 turns are stateless per tick, continuity lives on disk (logs, receipts, now.md).
9387
9395
  if (!skipWorker && !atris2Runner && !drillRunner && !sessionId && !pendingSessionId) {
9388
9396
  pendingSessionId = crypto.randomUUID();
9389
9397
  mission = saveMission({ ...mission, pending_session_id: pendingSessionId }, cwd, 'mission_session_pending', { session_id: pendingSessionId }).mission;
@@ -9464,7 +9472,7 @@ async function executeMissionRunTicksPhase(context) {
9464
9472
  // the auto runner has already resolved this tick to a *different* ready engine
9465
9473
  // (the walled one is now credit_out and excluded from resolution), the run-wide
9466
9474
  // cooldown no longer applies. Clear it so the fresh engine runs now instead of
9467
- // the loop sleeping through or pausing on `rate-limit-exceeded-wall` for a
9475
+ // the loop sleeping through, or pausing on `rate-limit-exceeded-wall` for, a
9468
9476
  // wall this engine never hit.
9469
9477
  if (lastRateLimit && rateLimitedEngineId && tickEngineId && tickEngineId !== rateLimitedEngineId) {
9470
9478
  result.engine_swapped_from = rateLimitedEngineId;
@@ -9663,8 +9671,8 @@ async function executeMissionRunTicksPhase(context) {
9663
9671
  // held, so the next resume fails with "already in use". A session
9664
9672
  // that was cleaned up between ticks fails the resume the other way,
9665
9673
  // with "No conversation found with session ID". Both mean the stored
9666
- // id is dead. Session continuity is disposable mission state lives
9667
- // on disk (receipts, logs, now.md) so rotate to a fresh id instead
9674
+ // id is dead. Session continuity is disposable, mission state lives
9675
+ // on disk (receipts, logs, now.md), so rotate to a fresh id instead
9668
9676
  // of grinding the repeated-error breaker on a stale session.
9669
9677
  const staleSession = /session id .* is already in use/i.test(claudeResult.stderr || '')
9670
9678
  || /no conversation found with session id/i.test(claudeResult.stderr || '');
@@ -9692,7 +9700,7 @@ async function executeMissionRunTicksPhase(context) {
9692
9700
  break;
9693
9701
  }
9694
9702
  } else if (sessionId && claudeResult.sessionIds.length > 0 && !claudeResult.sessionIds.includes(sessionId)) {
9695
- // session_id mismatch on a resumed session abort run
9703
+ // session_id mismatch on a resumed session, abort run
9696
9704
  pauseReason = 'session-id-mismatch';
9697
9705
  break;
9698
9706
  }
@@ -9741,7 +9749,7 @@ async function executeMissionRunTicksPhase(context) {
9741
9749
  || worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
9742
9750
 
9743
9751
  // Layer classification needs the receipt text AND the worktree receipt, so it
9744
- // runs here after both exist covering the claude and atris2 branches alike.
9752
+ // runs here, after both exist, covering the claude and atris2 branches alike.
9745
9753
  if (!(cachedStep && cachedStep.tick && cachedStep.tick.layer)) {
9746
9754
  const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
9747
9755
  const layerInfo = extractLayerFromReceiptText(tickReceiptText, tickWorktree?.new_since_baseline_sample);
@@ -9770,7 +9778,7 @@ async function executeMissionRunTicksPhase(context) {
9770
9778
  const fullBudgetMode = budgetRemainingSeconds > 0;
9771
9779
  // Worker may call `mission complete` mid-tick. Re-read disk so we do not
9772
9780
  // demote terminal status back to ready (always-on + verifier-pass used to
9773
- // reopen closed rooms mission 32 complete@19:23:33 → ready@19:23:48).
9781
+ // reopen closed rooms, mission 32 complete@19:23:33 → ready@19:23:48).
9774
9782
  const latestOnDisk = loadMissionMap(cwd).get(mission.id) || mission;
9775
9783
  const alreadyTerminal = TERMINAL_STATUSES.has(String(latestOnDisk.status || ''));
9776
9784
  let newStatus = (verifierResult?.passed && mission.always_on) ? 'ready' :
@@ -9863,7 +9871,7 @@ async function executeMissionRunTicksPhase(context) {
9863
9871
  ranTicks++;
9864
9872
  backoffAttempt = 0;
9865
9873
  } else if (result.status === 'errored' && result.reason !== 'claude-session-busy') {
9866
- // A rotated session is already healed the next tick starts on a
9874
+ // A rotated session is already healed, the next tick starts on a
9867
9875
  // fresh id, so backing off just burns wall clock. If rotation itself
9868
9876
  // keeps failing, the repeated-error breaker below still stops the run.
9869
9877
  backoffAttempt++;
@@ -9887,7 +9895,7 @@ async function executeMissionRunTicksPhase(context) {
9887
9895
  if (consecutiveIdenticalSummaryTicks(ticks) >= 3) { pauseReason = 'stuck-repeating'; break; }
9888
9896
  // A retired/inaccessible model is deterministic: the id is fixed for the run, so
9889
9897
  // every remaining tick (and every future cron firing) fails identically. Backoff
9890
- // only slows the bleeding. Stop on first detection and surface the dead id
9898
+ // only slows the bleeding. Stop on first detection and surface the dead id,
9891
9899
  // CLI-245 named this failure; this stops the loop from grinding on it forever.
9892
9900
  if (result.status === 'errored' && result.reason === 'model-unavailable') { pauseReason = 'model-unavailable'; break; }
9893
9901
  // Any OTHER error that recurs identically (claude-timeout, atris2-error, claude-error)
@@ -10054,12 +10062,12 @@ function completeMissionRunPhase(context) {
10054
10062
  }
10055
10063
  if (context.handled) return;
10056
10064
 
10057
- // BCK-1324: no-progress is a clean, honest stop the run did what it
10065
+ // BCK-1324: no-progress is a clean, honest stop, the run did what it
10058
10066
  // could and correctly recognized there was nothing left to do. It is NOT
10059
10067
  // a failure/blocker: pausing it (resumable, retried by cron/self-drive)
10060
10068
  // or dispatching handleMissionBlocker (files a fleet task, dispatches an
10061
10069
  // engine) would recreate the exact busywork loop this guard exists to
10062
- // stop. Give it its own branch, shaped like stopMission()'s stop path
10070
+ // stop. Give it its own branch, shaped like stopMission()'s stop path,
10063
10071
  // status=stopped, a receipt, no escalation, no blocker.
10064
10072
  if (pauseReason === 'no-progress') {
10065
10073
  const stoppedAt = stampIso();
@@ -10077,7 +10085,7 @@ function completeMissionRunPhase(context) {
10077
10085
  stop_reason: noProgressReason,
10078
10086
  receipt_path: noProgressReceipt || mission.receipt_path || null,
10079
10087
  worktree_baseline: baselineSummary || mission.worktree_baseline || null,
10080
- next_action: 'mission stopped: no progress inspect the last few receipts, then start a fresh mission or resume with new context',
10088
+ next_action: 'mission stopped: no progress, inspect the last few receipts, then start a fresh mission or resume with new context',
10081
10089
  }, cwd, 'mission_run_stopped_no_progress', {
10082
10090
  reason: noProgressReason,
10083
10091
  idle_ticks: idleCount,
@@ -10106,7 +10114,7 @@ function completeMissionRunPhase(context) {
10106
10114
  }).mission;
10107
10115
  }
10108
10116
 
10109
- // 'no-progress' is a clean stop the run diagnosed itself never route it
10117
+ // 'no-progress' is a clean stop the run diagnosed itself, never route it
10110
10118
  // through handleMissionBlocker (that would file a blocker task and
10111
10119
  // dispatch an engine to "fix" a mission that correctly stopped itself).
10112
10120
  const blockerReason = ['no-progress', 'budget-exhausted'].includes(pauseReason)
@@ -10226,7 +10234,7 @@ function tickMission(args) {
10226
10234
  }
10227
10235
 
10228
10236
  try {
10229
- // Re-read inside the lock the initial resolveMission ran before we held it.
10237
+ // Re-read inside the lock, the initial resolveMission ran before we held it.
10230
10238
  mission = resolveMission(mission.id) || mission;
10231
10239
 
10232
10240
  if (['complete', 'stopped'].includes(mission.status)) {
@@ -10241,7 +10249,7 @@ function tickMission(args) {
10241
10249
  // budget window from now BEFORE the budget check below reads it. Without this,
10242
10250
  // a spend-full-budget mission whose original window already elapsed, resumed
10243
10251
  // via `mission tick` rather than `mission run`, would compute zero remaining
10244
- // budget on this very tick and land immediately the footgun `mission run`
10252
+ // budget on this very tick and land immediately, the footgun `mission run`
10245
10253
  // already closes. Stamping resumed_at here makes both resume paths agree.
10246
10254
  mission = saveMission({
10247
10255
  ...mission,
@@ -10486,7 +10494,7 @@ function receiptShowsPass(receipt) {
10486
10494
  || result?.tick?.verifier_passed === true;
10487
10495
  }
10488
10496
 
10489
- // Terminal gate: a verifier mission may only complete on real evidence a
10497
+ // Terminal gate: a verifier mission may only complete on real evidence, a
10490
10498
  // passing receipt belonging to this mission, or durable state showing the
10491
10499
  // verifier passed. Mirrors the task plane's proof-only accept guard so the
10492
10500
  // final transition consumes the receipts instead of trusting free text.
@@ -10658,7 +10666,7 @@ function stopMission(args) {
10658
10666
  }
10659
10667
 
10660
10668
  // A mission parked in paused/planning/ready and untouched for a week is
10661
- // abandoned in practice nobody resumes it, and each one is a line the
10669
+ // abandoned in practice, nobody resumes it, and each one is a line the
10662
10670
  // operator re-reads forever. The daily autoland tick expires them to
10663
10671
  // stopped with a revive hint; running missions and anything touched
10664
10672
  // recently are never aged out, and a tick on an expired id revives it.
@@ -10700,7 +10708,7 @@ function expireStaleMissions(root = process.cwd(), { idleDays = MISSION_IDLE_EXP
10700
10708
  for (const mission of listMissions(root)) {
10701
10709
  const status = String(mission.status || '').toLowerCase();
10702
10710
  if (!statuses.has(status)) continue;
10703
- // updated_at and paused_at are machine-polluted status renders and
10711
+ // updated_at and paused_at are machine-polluted, status renders and
10704
10712
  // goal controllers re-save parked missions daily, so a mission nobody
10705
10713
  // has run since May reads as "touched today". Real activity is the
10706
10714
  // last tick (or creation, for missions that never ran).
@@ -10737,7 +10745,7 @@ function expireStaleMissions(root = process.cwd(), { idleDays = MISSION_IDLE_EXP
10737
10745
  }
10738
10746
 
10739
10747
  // Zombie-mission reap: a mission left paused past a short leash (48h default)
10740
- // is dead in practice long before the 7-day general idle expiry ever fires
10748
+ // is dead in practice long before the 7-day general idle expiry ever fires,
10741
10749
  // nobody is coming back to it inside a session, and it just sits on
10742
10750
  // `mission list` as noise. Narrower than expireStaleMissions on purpose: only
10743
10751
  // `paused`, never planning/ready, so the weekly cadence for those is unchanged.
@@ -11190,7 +11198,7 @@ function classifyPathsByLayer(paths) {
11190
11198
  return winnerLayer ? { layer: winnerLayer, source: 'fallback' } : { layer: null, source: 'unknown' };
11191
11199
  }
11192
11200
 
11193
- // `atris mission layers` per-layer growth curve across tick receipts. The member
11201
+ // `atris mission layers`, per-layer growth curve across tick receipts. The member
11194
11202
  // proof standard says: if every tick is one layer and none touch the others, the
11195
11203
  // loop is doing work but not getting smarter. This makes that check one command.
11196
11204
  function layersMission(args) {
@@ -11251,7 +11259,7 @@ function layersMission(args) {
11251
11259
  ...LAYERS.map((l) => ` ${l.padEnd(12)} ${String(byLayer[l]).padStart(3)}${byLayer[l] ? ' ' + '█'.repeat(Math.min(byLayer[l], 40)) : ''}`),
11252
11260
  ...(untagged ? [` untagged ${String(untagged).padStart(3)} (pre-layer receipts or missing tag)`] : []),
11253
11261
  ` provenance: explicit ${bySource.explicit}, explicit-inline ${bySource['explicit-inline']}, fallback ${bySource.fallback}`,
11254
- ...(skewed ? [` rebalance: ${Math.round((byLayer[dominant] / tagged) * 100)}% of tagged ticks are "${dominant}" the proof standard wants the other layers moving too`] : []),
11262
+ ...(skewed ? [` rebalance: ${Math.round((byLayer[dominant] / tagged) * 100)}% of tagged ticks are "${dominant}", the proof standard wants the other layers moving too`] : []),
11255
11263
  ];
11256
11264
  printJsonOrText({ ok: true, since: sinceRaw || null, total, tagged, untagged, by_layer: byLayer, by_source: bySource, dominant: tagged ? dominant : null, skewed }, lines, asJson);
11257
11265
  }
@@ -11340,7 +11348,7 @@ function findMissionAcrossWorktrees(ref, root = process.cwd()) {
11340
11348
  return null;
11341
11349
  }
11342
11350
 
11343
- // atris mission ping <id> "<message>" leave a note the mission's next tick
11351
+ // atris mission ping <id> "<message>", leave a note the mission's next tick
11344
11352
  // reads (and consumes) as operator direction. This is how you talk to an
11345
11353
  // always-on member mid-run without stopping it.
11346
11354
  function inspectMission(args) {
@@ -11419,7 +11427,7 @@ function pingMission(args, opts = {}) {
11419
11427
  if (asJson) {
11420
11428
  console.log(JSON.stringify({ ok: true, action: 'mission_ping', mission_id: saved.id, pending_pings: pending, ping }));
11421
11429
  } else {
11422
- console.log(`pinged ${saved.id} the next tick reads it (${pending} unread).`);
11430
+ console.log(`pinged ${saved.id}, the next tick reads it (${pending} unread).`);
11423
11431
  }
11424
11432
  }
11425
11433
  return saved;
@@ -11569,7 +11577,7 @@ function missionCommand(args) {
11569
11577
  if (first && !first.startsWith('-')) {
11570
11578
  // Shortcut forms: a mission id, or an objective (quoted or spread
11571
11579
  // across argv: `atris mission fix the issue`). Two shapes are
11572
- // mistyped verbs, not objectives a later arg naming a mission id
11580
+ // mistyped verbs, not objectives, a later arg naming a mission id
11573
11581
  // (`mission say <id> ...`), and a single bare word with no objective
11574
11582
  // after it. Creating a mission from those silently is worse than erroring.
11575
11583
  const positionals = args.filter((value) => !String(value).startsWith('-'));
@@ -664,7 +664,7 @@ async function runOneLap(ask, options = {}) {
664
664
  ownCli: runCli,
665
665
  log: (line) => {
666
666
  const clean = String(line || '')
667
- .replace(/[—–]/g, '-')
667
+ .replace(/[\u2014\u2013]/g, '-')
668
668
  .replace(/[✓✔]/g, 'passed')
669
669
  .replace(/[✗✖]/g, 'failed')
670
670
  .replace(/[⏸·→]/g, '-')