atris 3.37.1 → 3.40.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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
package/bin/atris.js CHANGED
@@ -504,7 +504,7 @@ function showHelp() {
504
504
  console.log(' activate - Load Atris context');
505
505
  console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
506
506
  console.log(' stream - Watch the whole team work live in one terminal');
507
- console.log(' team - Show who is awake, what they are doing, and running loops');
507
+ console.log(' team - One team view: members, roles, engine assignments (presence for live)');
508
508
  console.log(' watch - Turn one sentence into an always-on background watcher');
509
509
  console.log(' ctop - Show a process-first live agent CPU/memory view');
510
510
  console.log(' launchpad - Show the next action from local brain, task, mission, and proof state');
@@ -519,14 +519,18 @@ function showHelp() {
519
519
  console.log(' close - track open loops with deadlines and daily escalation');
520
520
  console.log(' harvest - Find bugs and next actions from receipts, run logs, and thinking');
521
521
  console.log(' verify - Validate work is done (tests, MAP.md, changes)');
522
+ console.log(' slop - Deterministic slop detector: frontend/prose tells, dead code (slop dead --exports)');
523
+ console.log(' voice - Scan binary voice tells or judge reply shape and plainness');
524
+ console.log(' recover - Explain stopped missions; --apply performs safe recovery only');
522
525
  console.log(' task - Local agent task plane (atomic claims, TODO import)');
523
- console.log(' golden path (zero human turns):');
526
+ console.log(' golden path (one tick, by cron or by hand):');
524
527
  console.log(' atris task delegate "fix the login bug" --to <member>');
525
528
  console.log(' atris task claim <id> --as <member>');
526
529
  console.log(' ... build ...');
527
530
  console.log(' atris task ready <id> --verify');
528
531
  console.log(' atris autoland tick # second check runs, task lands');
529
532
  console.log(' mission - Goal + loop + member owner + verifier + receipt; --budget quick|long|deep sets bounded tiers');
533
+ console.log(' decide - Answer open mission asks and send the decision to the next tick');
530
534
  console.log(' release - Tag release, bump version, create GitHub release, draft /launch');
531
535
  console.log(' learn - Project learnings (patterns, pitfalls, preferences)');
532
536
  console.log(' study - On-demand learning feed: ingest topic, start server, open browser');
@@ -535,10 +539,12 @@ function showHelp() {
535
539
  console.log(' avail - Booking availability (/book/{username} weekly windows)');
536
540
  console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
537
541
  console.log(' lesson - Append a one-line lesson to atris/lessons.md (mine: distill receipts/episodes/scorecards into policy lessons)');
542
+ console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
543
+ console.log(' teach - Turn a bad turn into a failing benchmark, then a permanent guard (red gate: no promotion without a proven failure)');
538
544
  console.log(' ingest - Local-first wiki ingest into atris/wiki/');
539
545
  console.log(' query - Local-first wiki query against atris/wiki/');
540
546
  console.log(' lint - Local-first wiki lint for atris/wiki/');
541
- console.log(' loop - Local wiki upkeep loop (stale pages, orphans, next ingest)');
547
+ console.log(' loop - Recurring loops front door (wiki upkeep lives under: loop wiki)');
542
548
  console.log(' write - Guided writing sessions: you write every word, atris structures + reviews');
543
549
  console.log('');
544
550
  console.log('Optional helpers:');
@@ -548,8 +554,9 @@ function showHelp() {
548
554
  console.log(' worktree - Isolated Git worktrees plus guarded ship/merge for parallel agents');
549
555
  console.log(' land - The landing: what is actually done vs still in the air; --reap backs up + clears overdue');
550
556
  console.log(' drive - One self-driving tick: mission doctor -> auto-fix -> count disengagements');
551
- console.log(' autoland - Approve the policy once; certified work lands itself, you keep irreversible calls');
557
+ console.log(` autoland - Approve the policy once; ${require('../lib/autoland').certifiedWorkLandsPhrase(process.cwd())}, you keep irreversible calls`);
552
558
  console.log(' engine - Engine registry: list/resolve roles, health flips, default engine, `engine test`, and dispatch flights');
559
+ console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
553
560
  console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
554
561
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
555
562
  console.log(' youtube - Process YouTube videos with timestamped transcript-first analysis');
@@ -575,6 +582,7 @@ function showHelp() {
575
582
  console.log('Sync:');
576
583
  console.log(' pull - Pull journals + member data from cloud');
577
584
  console.log(' push - Push workspace files to cloud');
585
+ console.log(' sync-checkout - fetch and fast-forward a clean current branch');
578
586
  console.log(' cloud - Delete cloud files not present locally (cloud clean --dry-run|--yes)');
579
587
  console.log(' live - Keep a business brain fresh (doctor, pull, watch, push)');
580
588
  console.log(' clean-workspace <slug> - Analyze & remove junk files from a workspace (alias: cw)');
@@ -583,7 +591,7 @@ function showHelp() {
583
591
  console.log(' browse [query] - Discover workspace templates');
584
592
  console.log(' fork <template> - Clone a template into a new workspace');
585
593
  console.log(' publish - Share your workspace as a template');
586
- console.log(' pack - Publish or install an Atris brain zip');
594
+ console.log(' pack - Craft, publish, share, browse, or install an Atris brain zip');
587
595
  console.log(' sleep [business|member] [--loop id] - Pause compute or flip a member switch');
588
596
  console.log(' wake [business|member] [--loop id] - Resume compute or flip a member switch');
589
597
  console.log('');
@@ -592,6 +600,8 @@ function showHelp() {
592
600
  console.log(' business onboard - Onboard from sparse input (--name, --website, --contact)');
593
601
  console.log(' business add <slug> - Connect a business');
594
602
  console.log(' business list - Show connected businesses');
603
+ console.log(' business room [slug] - Show the live room from the production API');
604
+ console.log(' business store on - Enable the room store with its current catalog');
595
605
  console.log(' business remove <slug> - Disconnect a business');
596
606
  console.log(' business team [slug] - Show members, roles, and admin access');
597
607
  console.log(' business health <slug> - Health report (members, workspace, issues)');
@@ -1617,6 +1627,11 @@ function showWelcomeVisualization() {
1617
1627
  console.log(` learned \"${latestBriefTitle}\" overnight -> atris/wiki/briefs/${latestBriefName}`);
1618
1628
  }
1619
1629
 
1630
+ const checkoutWarning = require('../lib/checkout-sync').checkoutBehindMessage(cwd);
1631
+ if (checkoutWarning) {
1632
+ console.log(` ${checkoutWarning}`);
1633
+ }
1634
+
1620
1635
  // Show the work itself, not counts. A newcomer in any domain (code, docs,
1621
1636
  // a travel plan) should read actual task names and know what's happening.
1622
1637
  // Waiting-on-you comes first: the one thing only a human can do.
@@ -1694,6 +1709,16 @@ function showWelcomeVisualization() {
1694
1709
  console.log(row('tidy', tidyBits.join(', ')));
1695
1710
  }
1696
1711
 
1712
+ let wikiNudge = '';
1713
+ try {
1714
+ wikiNudge = require('../lib/wiki').wikiMetabolismNudge(cwd);
1715
+ } catch {
1716
+ wikiNudge = '';
1717
+ }
1718
+ if (wikiNudge) {
1719
+ console.log(` ${wikiNudge}`);
1720
+ }
1721
+
1697
1722
  console.log(row('logs', journalEntries > 0
1698
1723
  ? `${journalEntries} note${journalEntries === 1 ? '' : 's'} today`
1699
1724
  : 'nothing yet today'));
@@ -1792,6 +1817,21 @@ if (command === 'init') {
1792
1817
  Promise.resolve(require('../commands/bench').benchCommand(process.argv.slice(3)))
1793
1818
  .then((code) => process.exit(code || 0))
1794
1819
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(2); });
1820
+ } else if (command === 'router') {
1821
+ const code = require('../commands/router').routerCommand(process.argv.slice(3));
1822
+ if (Number.isInteger(code) && code !== 0) process.exit(code);
1823
+ } else if (command === 'decide') {
1824
+ // process.exit() can outrun a piped stdout: writes beyond the 64KB pipe
1825
+ // buffer are async, so large --json payloads truncate at 64KB multiples.
1826
+ // Queue an empty write and exit from its callback — it fires only after
1827
+ // every earlier buffered write has drained (BCK-1306).
1828
+ const exitAfterStdoutDrain = (code) => {
1829
+ if (process.stdout.writableLength === 0) process.exit(code);
1830
+ else process.stdout.write('', () => process.exit(code));
1831
+ };
1832
+ Promise.resolve(require('../commands/decide').decideCommand(process.argv.slice(3)))
1833
+ .then(() => exitAfterStdoutDrain(process.exitCode || 0))
1834
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); exitAfterStdoutDrain(1); });
1795
1835
  } else if (command === 'mission') {
1796
1836
  // process.exit() can outrun a piped stdout: writes beyond the 64KB pipe
1797
1837
  // buffer are async, so large --json payloads truncate at 64KB multiples.
@@ -1804,6 +1844,10 @@ if (command === 'init') {
1804
1844
  Promise.resolve(require('../commands/mission').missionCommand(process.argv.slice(3)))
1805
1845
  .then(() => exitAfterStdoutDrain(process.exitCode || 0))
1806
1846
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); exitAfterStdoutDrain(1); });
1847
+ } else if (command === 'recover') {
1848
+ Promise.resolve(require('../commands/recover').recoverCommand(process.argv.slice(3)))
1849
+ .then((code) => process.exit(code || 0))
1850
+ .catch((err) => { console.error(`\nerror: ${err.message || err}`); process.exit(1); });
1807
1851
  } else if (command === 'agents') {
1808
1852
  // Glanceable view of every member's state: stuck, waiting on you, working, resting.
1809
1853
  const code = require('../commands/agents').agentsCommand(process.argv.slice(3));
@@ -1931,8 +1975,17 @@ if (command === 'init') {
1931
1975
  logCmd();
1932
1976
  }
1933
1977
  } else if (command === 'logs') {
1978
+ const logsArgs = process.argv.slice(3);
1979
+ if (logsArgs.includes('--help') || logsArgs.includes('-h') || logsArgs[0] === 'help') {
1980
+ console.log('Usage: atris logs [--date YYYY-MM-DD] [--json]');
1981
+ console.log('');
1982
+ console.log(' atris logs Show today\'s journal entries, completed receipts, and inbox in one view');
1983
+ console.log(' atris logs --date <day> Show the digest for a specific day');
1984
+ console.log(' atris logs --json Emit the digest as JSON');
1985
+ process.exit(0);
1986
+ }
1934
1987
  try {
1935
- require('../commands/log').logsDigest(process.argv.slice(3));
1988
+ require('../commands/log').logsDigest(logsArgs);
1936
1989
  process.exit(0);
1937
1990
  } catch (error) {
1938
1991
  console.error(error.message || String(error));
@@ -1956,6 +2009,9 @@ if (command === 'init') {
1956
2009
  activateCmd();
1957
2010
  } else if (command === 'watch') {
1958
2011
  require('../commands/watch').watchAtris();
2012
+ } else if (command === 'sync-checkout') {
2013
+ const code = require('../commands/sync-checkout').syncCheckoutCommand(process.argv.slice(3));
2014
+ process.exit(code);
1959
2015
  } else if (command === 'update' || command === 'sync') {
1960
2016
  const args = process.argv.slice(3);
1961
2017
  const firstSyncArg = process.argv[3];
@@ -2514,6 +2570,10 @@ if (command === 'init') {
2514
2570
  require('../commands/gm').gmCommand(...process.argv.slice(3))
2515
2571
  .then(() => process.exit(0))
2516
2572
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
2573
+ } else if (command === 'game') {
2574
+ require('../commands/game').gameCommand(process.argv.slice(3))
2575
+ .then((status) => process.exit(status === 1 ? 1 : 0))
2576
+ .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
2517
2577
  } else if (command === 'github') {
2518
2578
  const status = require('../commands/github').githubCommand(process.argv.slice(3));
2519
2579
  process.exit(status);
@@ -2590,6 +2650,14 @@ if (command === 'init') {
2590
2650
  const subcommand = process.argv[3];
2591
2651
  const args = process.argv.slice(4);
2592
2652
  require('../commands/lesson')(subcommand, ...args);
2653
+ } else if (command === 'taste') {
2654
+ Promise.resolve(require('../commands/taste').tasteCommand(process.argv.slice(3)))
2655
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2656
+ .catch((err) => { console.error(`taste could not continue: ${err.message || err}.`); process.exit(1); });
2657
+ } else if (command === 'teach') {
2658
+ const subcommand = process.argv[3];
2659
+ const args = process.argv.slice(4);
2660
+ require('../commands/teach')(subcommand, ...args);
2593
2661
  } else if (command === 'skill') {
2594
2662
  const subcommand = process.argv[3];
2595
2663
  const args = process.argv.slice(4);
@@ -2759,6 +2827,10 @@ if (command === 'init') {
2759
2827
  Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
2760
2828
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
2761
2829
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2830
+ } else if (command === 'voice') {
2831
+ Promise.resolve(require('../commands/voice').voiceCommand(process.argv.slice(3)))
2832
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2833
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2762
2834
  } else if (command === 'write') {
2763
2835
  // Write: guided writing sessions — human types every word, atris structures + reviews (plan-do-review for prose).
2764
2836
  Promise.resolve(require('../commands/write').writeCommand(process.argv.slice(3)))
@@ -110,6 +110,15 @@ function completionSentence(completions) {
110
110
  return humanSentence(`since last time: ${landed}; the biggest: ${biggest}.`);
111
111
  }
112
112
 
113
+ // STATUS.md bullets can carry paths and counts the gate would reject, so the
114
+ // narration only says that attention is needed, never quotes the bullet.
115
+ function wikiHealthSentence(wikiStatus) {
116
+ if (!wikiStatus || !Array.isArray(wikiStatus.bullets)) return null;
117
+ const needsAttention = wikiStatus.bullets.some((b) => /stale|orphan/i.test(b));
118
+ if (!needsAttention) return null;
119
+ return humanSentence('the wiki has pages going stale; refresh them: atris loop wiki');
120
+ }
121
+
113
122
  function activeWorkSentence(state, context) {
114
123
  const tasks = Array.isArray(context?.inProgressTasks) ? context.inProgressTasks : [];
115
124
  if (tasks.length) {
@@ -237,13 +246,14 @@ function activateAtris() {
237
246
  } catch {}
238
247
 
239
248
  let moves = [];
249
+ let clarityLine = null;
240
250
  try {
241
251
  const { nextMoves } = require('../lib/next-moves');
242
252
  const { profilePaths } = require('../lib/clarity');
243
253
  const root = process.cwd();
244
254
  moves = nextMoves(root, 3);
245
255
  const { profile, mdPath } = readActivationClarityProfile(root, profilePaths);
246
- formatClarityLine(profile, rel(mdPath));
256
+ clarityLine = formatClarityLine(profile, rel(mdPath));
247
257
  } catch { /* alive onboarding is best-effort; never block activate */ }
248
258
 
249
259
  let briefData = null;
@@ -258,7 +268,6 @@ function activateAtris() {
258
268
 
259
269
  void dateFormatted;
260
270
  void learningCount;
261
- void wikiStatus;
262
271
  fs.existsSync(personaFile);
263
272
  fs.existsSync(mapFile);
264
273
  void taskFilePath;
@@ -269,6 +278,7 @@ function activateAtris() {
269
278
  handoffSentence(handoffContent),
270
279
  completionSentence(recentCompletions),
271
280
  activeWorkSentence(state, context),
281
+ wikiHealthSentence(wikiStatus),
272
282
  ].filter(Boolean);
273
283
  if (Array.isArray(briefData?.waiting)) {
274
284
  const approvalCount = briefData.waiting.length;
@@ -284,6 +294,9 @@ function activateAtris() {
284
294
  statusLines.forEach((line) => console.log(line));
285
295
  }
286
296
 
297
+ console.log('');
298
+ console.log(clarityLine || 'Tip: run atris clarity once so agents learn how you work.');
299
+
287
300
  const nextMove = moves[0] || briefData?.moves?.[0];
288
301
  const nextMoveTitle = focusFragment(nextMove?.title, 100);
289
302
  if (nextMoveTitle) {
@@ -292,4 +305,4 @@ function activateAtris() {
292
305
  }
293
306
  }
294
307
 
295
- module.exports = { activateAtris };
308
+ module.exports = { activateAtris, wikiHealthSentence };
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { spawnSync } = require('child_process');
5
+ const { hasFlag: hasSingleFlag } = require('../lib/arg-parser');
5
6
 
6
7
  const SPAWN_SCHEMA = 'atris.agent_spawn.v1';
7
8
  const DOGFOOD_SCHEMA = 'atris.agent_cli_dogfood.v1';
@@ -63,8 +64,9 @@ function flagValue(args, names) {
63
64
  return null;
64
65
  }
65
66
 
66
- function hasFlag(args, names) {
67
- return names.some(name => args.includes(name));
67
+ // This command accepts alias lists, while the shared helper checks one name.
68
+ function hasAnyFlag(args, names) {
69
+ return names.some(name => hasSingleFlag(args, name));
68
70
  }
69
71
 
70
72
  function positionalArgs(args) {
@@ -148,11 +150,11 @@ function commandForEngine(request) {
148
150
  }
149
151
 
150
152
  function parseSpawnArgs(args = []) {
151
- const help = args.length === 0 || hasFlag(args, ['--help', '-h']) || args[0] === 'help';
153
+ const help = args.length === 0 || hasAnyFlag(args, ['--help', '-h']) || args[0] === 'help';
152
154
  if (help) return { help: true };
153
155
 
154
- const json = hasFlag(args, ['--json']);
155
- const dryRun = hasFlag(args, ['--dry-run']);
156
+ const json = hasAnyFlag(args, ['--json']);
157
+ const dryRun = hasAnyFlag(args, ['--dry-run']);
156
158
  const roleFlag = flagValue(args, ['--role']);
157
159
  const engine = String(flagValue(args, ['--engine']) || 'manual').toLowerCase();
158
160
  if (!ALLOWED_ENGINES.has(engine)) throw new Error(`Unknown engine: ${engine}`);
@@ -209,10 +211,10 @@ function showSpawnHelp(output = console.log, commandName = 'atris agent spawn')
209
211
  }
210
212
 
211
213
  function parseDogfoodArgs(args = []) {
212
- const help = hasFlag(args, ['--help', '-h']) || args[0] === 'help';
213
- const json = hasFlag(args, ['--json']);
214
- const live = hasFlag(args, ['--live']);
215
- const noWrite = hasFlag(args, ['--no-write']);
214
+ const help = hasAnyFlag(args, ['--help', '-h']) || args[0] === 'help';
215
+ const json = hasAnyFlag(args, ['--json']);
216
+ const live = hasAnyFlag(args, ['--live']);
217
+ const noWrite = hasAnyFlag(args, ['--no-write']);
216
218
  const model = flagValue(args, ['--model']) || 'glm-5.2';
217
219
  const timeoutRaw = Number(flagValue(args, ['--timeout']) || 45);
218
220
  const timeoutMs = Math.max(5, Math.min(300, timeoutRaw)) * 1000;
@@ -431,7 +433,7 @@ function agentSpawnCommand(args = [], deps = {}) {
431
433
  function agentSpawnListCommand(args = [], deps = {}) {
432
434
  const root = deps.root || process.cwd();
433
435
  const output = deps.output || ((line = '') => console.log(line));
434
- const json = hasFlag(args, ['--json']);
436
+ const json = hasAnyFlag(args, ['--json']);
435
437
  const requests = loadSpawnRequests(root).reverse();
436
438
  const payload = { ok: true, action: 'spawn_list', requests, state_path: spawnStatePath(root) };
437
439
  if (json) {
@@ -451,7 +453,7 @@ function agentSpawnListCommand(args = [], deps = {}) {
451
453
  function agentSpawnStatusCommand(args = [], deps = {}) {
452
454
  const root = deps.root || process.cwd();
453
455
  const output = deps.output || ((line = '') => console.log(line));
454
- const json = hasFlag(args, ['--json']);
456
+ const json = hasAnyFlag(args, ['--json']);
455
457
  const id = positionalArgs(args)[0];
456
458
  if (!id) throw new Error('Missing spawn id. Usage: atris agent spawn-status <id>');
457
459
  const request = loadSpawnRequests(root).find(req => req.id === id);
@@ -463,18 +465,11 @@ function agentSpawnStatusCommand(args = [], deps = {}) {
463
465
  }
464
466
 
465
467
  module.exports = {
466
- SPAWN_SCHEMA,
467
- SPAWN_STATE_REL,
468
468
  parseSpawnArgs,
469
- buildDelegatePrompt,
470
469
  commandForEngine,
471
470
  createSpawnRequest,
472
471
  agentDogfoodCommand,
473
- parseDogfoodArgs,
474
- runDogfoodEngine,
475
- loadSpawnRequests,
476
472
  agentSpawnCommand,
477
473
  agentSpawnListCommand,
478
474
  agentSpawnStatusCommand,
479
- showSpawnHelp,
480
475
  };
@@ -6,7 +6,7 @@ const os = require('os');
6
6
  const { spawnSync } = require('child_process');
7
7
 
8
8
  const autoland = require('../lib/autoland');
9
- const { gateForHuman } = require('../lib/voice-gate');
9
+ const { gateForHuman, plainLandingReason } = require('../lib/voice-gate');
10
10
  const { evaluateAutoAccept } = require('../lib/auto-accept-certified');
11
11
  const { operatorReady, hasAgentJargon, explainResult } = autoland;
12
12
  const MISSION_AUTO_VERIFY_STATUSES = new Set(['planning', 'paused', 'ready']);
@@ -436,26 +436,9 @@ function protectedReviewWaiting(root, policy = {}, waiting = []) {
436
436
  .filter((row) => !alreadyWaiting.has(row.ref));
437
437
  }
438
438
 
439
- function plainReason(reason) {
440
- const map = {
441
- denied_tag_billing: 'money: human decision required',
442
- denied_tag_deploy: 'a deploy: human decision required',
443
- denied_tag_security: 'security: human decision required',
444
- denied_tag_customer: 'customer-facing: human decision required',
445
- denied_tag_external: 'outward-facing: human decision required',
446
- denied_tag_feedback: 'customer feedback: human decision required',
447
- needs_second_reviewer_or_third_pass: 'needs one more independent check first',
448
- needs_independent_reviewer: 'built and judged by the same actor, needs an independent check',
449
- verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
450
- judge_equals_worker: 'built and judged by the same actor, hand the review to someone else',
451
- not_agent_certified: 'not certified yet',
452
- insufficient_review_passes: 'not enough review passes yet',
453
- strict_verify_missing: 'no recorded check command to re-run',
454
- verify_failed: 'its check command failed on re-run',
455
- proof_unmerged_or_draft_pr_boundary: 'its proof points at an unmerged draft',
456
- };
457
- return map[reason] || reason.replace(/_/g, ' ');
458
- }
439
+ // One reason table for every human-facing landing refusal lives in
440
+ // lib/voice-gate.js (plainLandingReason); this alias keeps call sites short.
441
+ const plainReason = plainLandingReason;
459
442
 
460
443
  function showStatus(root, args) {
461
444
  const json = args.includes('--json');
@@ -470,9 +453,13 @@ function showStatus(root, args) {
470
453
  const blocked = results.filter((r) => r.action !== 'would_accept');
471
454
  const tasks = readProjection(root);
472
455
  const waiting = autoland.waitingOnHuman(tasks);
473
- const heartbeatInstalled = typeof policy?.heartbeat_installed === 'boolean'
474
- ? policy.heartbeat_installed
475
- : null;
456
+ // Ask the crontab, not the policy file. The cached flag is written once at
457
+ // `autoland on` time, so it reports "unknown" for a cron that has been
458
+ // running hourly for weeks, and would keep reporting "running" for one
459
+ // someone deleted. A loop that cannot confirm its own liveness from
460
+ // evidence cannot be trusted to say it is healthy.
461
+ const heartbeatInstalled = autoland.heartbeatLiveness(root, policy);
462
+ const heartbeatLive = autoland.heartbeatIsLive(root);
476
463
 
477
464
  if (json) {
478
465
  console.log(JSON.stringify({
@@ -488,7 +475,7 @@ function showStatus(root, args) {
488
475
  }
489
476
 
490
477
  console.log('');
491
- console.log('autoland: certified work lands itself; you keep the irreversible calls');
478
+ console.log(`autoland: ${autoland.certifiedWorkLandsPhrase(root)}; you keep the irreversible calls`);
492
479
  console.log('');
493
480
  const policyOwner = String(policy?.enabled_by || 'unknown').trim() || 'unknown';
494
481
  const policyText = enabled
@@ -498,31 +485,33 @@ function showStatus(root, args) {
498
485
  : 'off - everything waits for you';
499
486
  console.log(` policy: ${policyText}`);
500
487
  if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
501
- const heartbeatText = heartbeatInstalled === true
502
- ? 'running hourly'
503
- : heartbeatInstalled === false
504
- ? 'not installed'
505
- : 'unknown - run atris autoland on to check and repair';
506
- console.log(` heartbeat: ${heartbeatText}`);
488
+ console.log(` heartbeat: ${autoland.heartbeatStatusText(root, policy)}`);
507
489
  if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
508
490
  const reapTrouble = autoland.readState(root).last_reap_error;
509
491
  if (reapTrouble) console.log(` cleanup trouble: landing sweep failed on ${reapTrouble.date} (${reapTrouble.error}) - run: atris land --reap`);
510
492
  console.log('');
511
- if (readyForRecheck.length > 0) {
512
- console.log(` ready for heartbeat recheck: ${readyForRecheck.length}`);
513
- for (const r of readyForRecheck.slice(0, 10)) console.log(` rechecks then lands ${r.ref}`);
514
- } else {
515
- console.log(' nothing is ready to land on its own right now.');
516
- }
517
493
  const humanOnly = blocked.filter((r) => String(r.reason || '').startsWith('denied_tag_'));
518
494
  const needsWork = blocked.filter((r) => !String(r.reason || '').startsWith('denied_tag_'));
519
495
  if (humanOnly.length > 0) {
520
- console.log(` protected reviews waiting on you: ${humanOnly.length}`);
521
- for (const r of humanOnly.slice(0, 10)) console.log(` waits for you ${r.ref} - ${plainReason(r.reason)}`);
496
+ console.log(` yours to decide first: ${humanOnly.length} protected ${humanOnly.length === 1 ? 'review' : 'reviews'}.`);
497
+ for (const r of humanOnly.slice(0, 10)) console.log(` ${r.ref} waits for you; ${plainReason(r.reason)}.`);
498
+ }
499
+ if (readyForRecheck.length > 0) {
500
+ if (heartbeatLive) {
501
+ console.log(` landing on their own after the hourly recheck: ${readyForRecheck.length}.`);
502
+ for (const r of readyForRecheck.slice(0, 10)) console.log(` ${r.ref} passes the recheck, then lands itself.`);
503
+ } else {
504
+ console.log(` ready to land ${autoland.STALE_HEARTBEAT_LANDING}: ${readyForRecheck.length}.`);
505
+ for (const r of readyForRecheck.slice(0, 10)) {
506
+ console.log(` ${r.ref} passes the recheck, then lands ${autoland.STALE_HEARTBEAT_LANDING}.`);
507
+ }
508
+ }
509
+ } else {
510
+ console.log(' nothing is ready to land on its own right now.');
522
511
  }
523
512
  if (needsWork.length > 0) {
524
- console.log(` not ready yet: ${needsWork.length}`);
525
- for (const r of needsWork.slice(0, 10)) console.log(` held back ${r.ref} - ${plainReason(String(r.reason || ''))}`);
513
+ console.log(` still needs work before it can land: ${needsWork.length}.`);
514
+ for (const r of needsWork.slice(0, 10)) console.log(` ${r.ref} stays put; ${plainReason(String(r.reason || ''))}.`);
526
515
  }
527
516
  if (waiting.length > 0) {
528
517
  console.log('');
@@ -696,6 +685,28 @@ function persistTickReceipt(root, receipt) {
696
685
  return receipt.receipt_path;
697
686
  }
698
687
 
688
+ // A tick that scans a backlog, lands none of it, and prints only zeros is
689
+ // indistinguishable from a tick with no work. Say the standing numbers out
690
+ // loud, and call out the two states that mean the loop is wedged rather than
691
+ // idle: work blocked behind a check the harness could not execute, and work
692
+ // that passed every gate and still did not land.
693
+ // Heartbeat liveness helpers live in lib/autoland.js so task ready copy and
694
+ // status share one gate (recent tick receipt within two hours).
695
+
696
+ function stuckBacklogNote(receipt) {
697
+ const parts = [];
698
+ const scanned = Number(receipt.scanned || 0);
699
+ const certified = Number(receipt.certified || 0);
700
+ if (scanned > 0 && receipt.landed.length === 0) {
701
+ parts.push(`${certified}/${scanned} certified in review, none landed`);
702
+ }
703
+ const unrunnable = Array.isArray(receipt.unrunnable_verifiers) ? receipt.unrunnable_verifiers.length : 0;
704
+ if (unrunnable > 0) {
705
+ parts.push(`${unrunnable} blocked by a check this machine could not run`);
706
+ }
707
+ return parts.length ? `, ${parts.join(', ')}` : '';
708
+ }
709
+
699
710
  function digestTickStatus(receipt) {
700
711
  if (receipt.digest_sent) return 'sent';
701
712
  if (receipt.digest_due) return 'saved';
@@ -734,6 +745,23 @@ function runTickBody(root, { json, policy, receipt }) {
734
745
  .filter((r) => r.action === 'skipped' && r.proof_state === 'suite_green_citation_required')
735
746
  .map((r) => ({ ref: r.ref, reason: r.reason }));
736
747
  if (citationBlocks.length) receipt.citation_blocks = citationBlocks;
748
+ // A verifier the machine cannot execute is a broken harness, not a verdict
749
+ // on the diff. It must never sit quietly in the skipped pile: surface it
750
+ // as an alarm so a wedged loop is visible on the first tick, not the 480th.
751
+ // Not filtered by action: an unrunnable check surfaces as `skipped` from
752
+ // the eligibility gate and as `revised`/`revise_failed` from the landing
753
+ // gate. The reason is the signal, not the lane it came through.
754
+ const unrunnable = results
755
+ .filter((r) => r && r.action !== 'accepted' && r.reason === 'verify_unrunnable')
756
+ .map((r) => ({
757
+ ref: r.ref,
758
+ cause: r.unrunnable_cause || 'unknown',
759
+ command: r.command || r.verify || null,
760
+ }));
761
+ if (unrunnable.length) {
762
+ receipt.unrunnable_verifiers = unrunnable;
763
+ receipt.alarms = Number(receipt.alarms || 0) + unrunnable.length;
764
+ }
737
765
  receipt.certified = parsed.certified ?? null;
738
766
  receipt.scanned = parsed.scanned ?? null;
739
767
  receipt.revised = parsed.revised ?? null;
@@ -956,7 +984,12 @@ function runTickBody(root, { json, policy, receipt }) {
956
984
  const heldNote = receipt.missions_held ? `, held ${receipt.missions_held} human-blocked mission${receipt.missions_held === 1 ? '' : 's'}` : '';
957
985
  const wishNote = `, ${wishSweepSummaryLine(receipt.wish_dispatch, receipt.wish_dispatch_error)}`;
958
986
  const receiptNote = receipt.receipt_path ? `, receipt ${receipt.receipt_path}` : '';
959
- const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${receiptNote}`;
987
+ // Deltas alone ("0 certified, 0 landed") are true and useless: they read
988
+ // as "nothing to do" whether the queue is empty or 19-deep and wedged.
989
+ // Always print the standing backlog, and name work that cleared every
990
+ // gate and still did not land — that is the shape of a stuck loop.
991
+ const stuckNote = stuckBacklogNote(receipt);
992
+ const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${receiptNote}`;
960
993
  console.log(gateForHuman(summary).text);
961
994
  for (const fulfilled of receipt.wish_dispatch?.fulfilled_results || []) {
962
995
  if (fulfilled.review_ask) console.log(fulfilled.review_ask);
@@ -967,7 +1000,7 @@ function runTickBody(root, { json, policy, receipt }) {
967
1000
 
968
1001
  function showHelp() {
969
1002
  console.log('');
970
- console.log('atris autoland: you approve the policy once; certified work lands itself');
1003
+ console.log(`atris autoland: you approve the policy once; ${autoland.certifiedWorkLandsPhrase(process.cwd())}`);
971
1004
  console.log('');
972
1005
  console.log('finished work that passed its checks and two independent reviews lands');
973
1006
  console.log('automatically with a receipt. money, deploys, security, customer, and');
@@ -1014,18 +1047,14 @@ function autolandCommand(args = []) {
1014
1047
 
1015
1048
  module.exports = {
1016
1049
  autolandCommand,
1017
- attachedTasksForMission,
1018
1050
  digestNextMoves,
1019
1051
  digestStoryRows,
1020
1052
  digestTickStatus,
1021
1053
  explainResult,
1022
- missionReadyForClosedTaskVerify,
1023
1054
  operatorReady,
1024
1055
  readAcceptedTaskHistory,
1025
1056
  hasAgentJargon,
1026
1057
  runTickBody,
1027
- persistTickReceipt,
1028
1058
  sweepLanding,
1029
1059
  verifyClosedTaskMissions,
1030
- wishSweepSummaryLine,
1031
1060
  };
@@ -279,10 +279,6 @@ async function autopilotFront(args = []) {
279
279
  module.exports = {
280
280
  autopilotFront,
281
281
  maxLegsFlag,
282
- autopilotStop,
283
- autopilotStatus,
284
- legPlan,
285
- pickMember,
286
282
  stopRequested,
287
283
  requestStop,
288
284
  clearStop,
@@ -3825,7 +3825,6 @@ async function autopilotFromTodo(options = {}) {
3825
3825
  }
3826
3826
 
3827
3827
  module.exports = {
3828
- appendTickSummary,
3829
3828
  snapshotRepoHeads,
3830
3829
  diffAdvancedRepoHeads,
3831
3830
  reconcileTimedOutTick,
@@ -3833,10 +3832,7 @@ module.exports = {
3833
3832
  findCompletionReceipt,
3834
3833
  isDoPhaseTimeoutMessage,
3835
3834
  validateVerifyCommandShape,
3836
- askHuman,
3837
- askModel,
3838
3835
  autopilotAtris,
3839
- autopilotFromTodo,
3840
3836
  buildPrompt,
3841
3837
  pendingExperimentTargets,
3842
3838
  isLessonResolved,
@@ -3847,7 +3843,6 @@ module.exports = {
3847
3843
  parseLessons,
3848
3844
  pickUnresolvedFailLesson,
3849
3845
  runLessonDetector,
3850
- isStillTrue,
3851
3846
  getTaskAgeDays,
3852
3847
  getIdleTickCount,
3853
3848
  getRecentSignals,
@@ -3865,14 +3860,8 @@ module.exports = {
3865
3860
  renderHumanSuggestion,
3866
3861
  renderHumanTickIntro,
3867
3862
  proposeCandidateHorizons,
3868
- recordTickCommit,
3869
- regressionCheck,
3870
- repoMapAuditReportsClean,
3871
- isCleanMapBrokenRefFailLesson,
3872
- inlinePythonVerifyFailureNowPasses,
3873
3863
  runPlanReview,
3874
3864
  runTaskOnce,
3875
- buildPlanReviewPrompt,
3876
3865
  parseVerdict,
3877
3866
  scoreEndgameCandidates,
3878
3867
  suggestNextTask,
@@ -3882,7 +3871,6 @@ module.exports = {
3882
3871
  isPhaseKillError,
3883
3872
  execPhaseCommandSync,
3884
3873
  executePhaseDetailed,
3885
- autopilotGlassLogDir,
3886
3874
  buildAutopilotGlassLog,
3887
3875
  writeAutopilotGlassLog,
3888
3876
  lessonSlug