atris 3.37.0 → 3.38.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 (139) hide show
  1. package/README.md +2 -0
  2. package/atris/PERSONA.md +3 -7
  3. package/atris.md +10 -2
  4. package/ax +158 -33
  5. package/bin/atris.js +49 -2
  6. package/commands/activate.js +16 -3
  7. package/commands/agent-spawn.js +0 -7
  8. package/commands/autoland.js +92 -14
  9. package/commands/autopilot-front.js +0 -4
  10. package/commands/autopilot.js +0 -12
  11. package/commands/brain.js +0 -7
  12. package/commands/brief.js +0 -2
  13. package/commands/business-sync.js +0 -1
  14. package/commands/business.js +107 -12
  15. package/commands/clean.js +31 -2
  16. package/commands/close.js +46 -11
  17. package/commands/cloud.js +0 -1
  18. package/commands/codex-goal.js +0 -2
  19. package/commands/computer.js +6 -5
  20. package/commands/console.js +46 -9
  21. package/commands/deck.js +29 -8
  22. package/commands/dream.js +0 -7
  23. package/commands/drill.js +0 -1
  24. package/commands/engine.js +10 -18
  25. package/commands/experiments.js +0 -4
  26. package/commands/fleet-report.js +12 -4
  27. package/commands/game.js +261 -0
  28. package/commands/gm.js +10 -2
  29. package/commands/goal.js +0 -6
  30. package/commands/harvest.js +0 -1
  31. package/commands/improve.js +29 -8
  32. package/commands/init.js +18 -34
  33. package/commands/integrations.js +0 -7
  34. package/commands/land.js +71 -8
  35. package/commands/launchpad.js +0 -2
  36. package/commands/lesson.js +17 -7
  37. package/commands/loop-front.js +0 -2
  38. package/commands/loops.js +6 -5
  39. package/commands/meet.js +0 -2
  40. package/commands/member.js +68 -3
  41. package/commands/mission.js +359 -102
  42. package/commands/next.js +0 -1
  43. package/commands/now.js +0 -12
  44. package/commands/one-lap.js +8 -8
  45. package/commands/orb.js +0 -4
  46. package/commands/pack-craft.js +0 -1
  47. package/commands/pack.js +670 -77
  48. package/commands/play.js +0 -2
  49. package/commands/probe.js +10 -4
  50. package/commands/pulse.js +73 -48
  51. package/commands/push.js +0 -4
  52. package/commands/radar.js +14 -11
  53. package/commands/recover.js +340 -0
  54. package/commands/report.js +0 -1
  55. package/commands/router.js +319 -0
  56. package/commands/run-front.js +0 -2
  57. package/commands/search.js +0 -3
  58. package/commands/skill.js +7 -2
  59. package/commands/slop.js +225 -7
  60. package/commands/status.js +6 -0
  61. package/commands/stream.js +0 -2
  62. package/commands/study.js +0 -3
  63. package/commands/sync-checkout.js +51 -0
  64. package/commands/sync.js +11 -40
  65. package/commands/task.js +125 -39
  66. package/commands/teach.js +86 -0
  67. package/commands/unknowns.js +0 -10
  68. package/commands/update.js +0 -1
  69. package/commands/visualize.js +0 -5
  70. package/commands/watch.js +0 -6
  71. package/commands/wiki.js +41 -11
  72. package/commands/wish.js +0 -1
  73. package/commands/worktree.js +87 -14
  74. package/commands/xp.js +1 -6
  75. package/commands/youtube.js +0 -2
  76. package/lib/accept-verify-gate.js +122 -0
  77. package/lib/activity-stream.js +0 -2
  78. package/lib/auto-accept-certified.js +145 -2
  79. package/lib/autoland.js +0 -3
  80. package/lib/ax-auto-lane.js +94 -10
  81. package/lib/bench/context.js +0 -2
  82. package/lib/bench/report.js +0 -1
  83. package/lib/bench/runner.js +0 -11
  84. package/lib/brief-ledger.js +0 -6
  85. package/lib/business-simulate.js +382 -0
  86. package/lib/chat-log-scan.js +0 -5
  87. package/lib/checkout-sync.js +52 -0
  88. package/lib/claude-boot-block.js +48 -0
  89. package/lib/cloud-mission.js +0 -6
  90. package/lib/codex-flight.js +0 -1
  91. package/lib/conductor-artifacts.js +25 -0
  92. package/lib/context-gatherer.js +0 -1
  93. package/lib/deck-compose.js +0 -1
  94. package/lib/deck-history.js +0 -1
  95. package/lib/deck-layout.js +0 -5
  96. package/lib/deck-review.js +0 -7
  97. package/lib/deck-schema.js +0 -1
  98. package/lib/default-verifier.js +0 -2
  99. package/lib/engine-registry.js +16 -8
  100. package/lib/experiments/daily.js +0 -6
  101. package/lib/fleet.js +210 -27
  102. package/lib/functional-owner.js +0 -3
  103. package/lib/inspect-fields.js +0 -1
  104. package/lib/known-commands.js +2 -2
  105. package/lib/learnings.js +0 -2
  106. package/lib/loop-doctor.js +11 -17
  107. package/lib/manifest.js +0 -2
  108. package/lib/member-alive.js +0 -1
  109. package/lib/member-switches.js +0 -5
  110. package/lib/mission-artifact.js +0 -1
  111. package/lib/mission-protected-lane.js +293 -0
  112. package/lib/mission-room.js +0 -2
  113. package/lib/mission-runtime-loop.js +0 -1
  114. package/lib/next-moves.js +0 -18
  115. package/lib/official-cli-integration.js +0 -4
  116. package/lib/orb-context.js +6 -3
  117. package/lib/orb-scorecard.js +0 -1
  118. package/lib/outbound-send-gate.js +0 -6
  119. package/lib/permission-grants.js +0 -6
  120. package/lib/policy-lessons.js +0 -2
  121. package/lib/pulse.js +68 -15
  122. package/lib/review-integrity.js +0 -2
  123. package/lib/router-brain.js +0 -2
  124. package/lib/runner-command.js +0 -4
  125. package/lib/runtime-bootstrap.js +0 -1
  126. package/lib/scorecard.js +0 -1
  127. package/lib/self-drive.js +1 -1
  128. package/lib/state-detection.js +0 -6
  129. package/lib/task-db.js +0 -4
  130. package/lib/team-presence.js +0 -1
  131. package/lib/voice-gate.js +63 -2
  132. package/lib/wiki.js +152 -9
  133. package/lib/wish-audit.js +20 -6
  134. package/lib/wish-delegate.js +26 -20
  135. package/lib/wish-stats.js +0 -5
  136. package/lib/wish-store.js +0 -1
  137. package/lib/workspace-safety.js +0 -2
  138. package/lib/zip.js +0 -1
  139. package/package.json +1 -1
@@ -470,9 +470,12 @@ function showStatus(root, args) {
470
470
  const blocked = results.filter((r) => r.action !== 'would_accept');
471
471
  const tasks = readProjection(root);
472
472
  const waiting = autoland.waitingOnHuman(tasks);
473
- const heartbeatInstalled = typeof policy?.heartbeat_installed === 'boolean'
474
- ? policy.heartbeat_installed
475
- : null;
473
+ // Ask the crontab, not the policy file. The cached flag is written once at
474
+ // `autoland on` time, so it reports "unknown" for a cron that has been
475
+ // running hourly for weeks, and would keep reporting "running" for one
476
+ // someone deleted. A loop that cannot confirm its own liveness from
477
+ // evidence cannot be trusted to say it is healthy.
478
+ const heartbeatInstalled = heartbeatLiveness(root, policy);
476
479
 
477
480
  if (json) {
478
481
  console.log(JSON.stringify({
@@ -498,12 +501,7 @@ function showStatus(root, args) {
498
501
  : 'off - everything waits for you';
499
502
  console.log(` policy: ${policyText}`);
500
503
  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}`);
504
+ console.log(` heartbeat: ${heartbeatStatusText(root, policy)}`);
507
505
  if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
508
506
  const reapTrouble = autoland.readState(root).last_reap_error;
509
507
  if (reapTrouble) console.log(` cleanup trouble: landing sweep failed on ${reapTrouble.date} (${reapTrouble.error}) - run: atris land --reap`);
@@ -696,6 +694,68 @@ function persistTickReceipt(root, receipt) {
696
694
  return receipt.receipt_path;
697
695
  }
698
696
 
697
+ // A tick that scans a backlog, lands none of it, and prints only zeros is
698
+ // indistinguishable from a tick with no work. Say the standing numbers out
699
+ // loud, and call out the two states that mean the loop is wedged rather than
700
+ // idle: work blocked behind a check the harness could not execute, and work
701
+ // that passed every gate and still did not land.
702
+ // Installed and alive are different claims, and only one of them is worth
703
+ // printing. Deliberately does NOT shell out to crontab: `autoland status` is
704
+ // read-only and must stay fast (a stalled crontab would block it for the full
705
+ // 10s read timeout — see 'autoland status never probes a stalled crontab').
706
+ // Tick receipts are local file reads and are stronger evidence anyway: a
707
+ // crontab line proves configuration, a recent receipt proves the loop ran.
708
+ function heartbeatLiveness(root, policy) {
709
+ return typeof policy?.heartbeat_installed === 'boolean' ? policy.heartbeat_installed : null;
710
+ }
711
+
712
+ function lastTickAgeHours(root) {
713
+ try {
714
+ const runsDir = path.join(root, 'atris', 'runs');
715
+ const newest = fs.readdirSync(runsDir)
716
+ .filter((f) => f.startsWith('autoland-tick-') && f.endsWith('.json'))
717
+ .sort()
718
+ .pop();
719
+ if (!newest) return null;
720
+ const stamp = fs.statSync(path.join(runsDir, newest)).mtimeMs;
721
+ return (Date.now() - stamp) / 3_600_000;
722
+ } catch {
723
+ return null;
724
+ }
725
+ }
726
+
727
+ function heartbeatStatusText(root, policy) {
728
+ const ageHours = lastTickAgeHours(root);
729
+ const ageText = ageHours === null
730
+ ? null
731
+ : ageHours < 1 ? 'under an hour' : `${Math.floor(ageHours)}h`;
732
+ // Evidence first: a receipt from the last couple of hours proves the loop is
733
+ // alive no matter what the policy file remembers. The hourly cron makes two
734
+ // missed hours an outage rather than jitter — and a heartbeat that has gone
735
+ // quiet is the single most useful thing this line can say, because that is
736
+ // the state nobody notices.
737
+ if (ageHours !== null && ageHours <= 2) return `running hourly (last tick ${ageText} ago)`;
738
+ if (ageHours !== null) return `SILENT - last tick ${ageText} ago; run atris autoland tick`;
739
+ const installed = heartbeatLiveness(root, policy);
740
+ if (installed === false) return 'not installed - run atris autoland on';
741
+ if (installed === true) return 'installed, but no tick has ever run - run atris autoland tick';
742
+ return 'unknown - run atris autoland on to check and repair';
743
+ }
744
+
745
+ function stuckBacklogNote(receipt) {
746
+ const parts = [];
747
+ const scanned = Number(receipt.scanned || 0);
748
+ const certified = Number(receipt.certified || 0);
749
+ if (scanned > 0 && receipt.landed.length === 0) {
750
+ parts.push(`${certified}/${scanned} certified in review, none landed`);
751
+ }
752
+ const unrunnable = Array.isArray(receipt.unrunnable_verifiers) ? receipt.unrunnable_verifiers.length : 0;
753
+ if (unrunnable > 0) {
754
+ parts.push(`${unrunnable} blocked by a check this machine could not run`);
755
+ }
756
+ return parts.length ? `, ${parts.join(', ')}` : '';
757
+ }
758
+
699
759
  function digestTickStatus(receipt) {
700
760
  if (receipt.digest_sent) return 'sent';
701
761
  if (receipt.digest_due) return 'saved';
@@ -734,6 +794,23 @@ function runTickBody(root, { json, policy, receipt }) {
734
794
  .filter((r) => r.action === 'skipped' && r.proof_state === 'suite_green_citation_required')
735
795
  .map((r) => ({ ref: r.ref, reason: r.reason }));
736
796
  if (citationBlocks.length) receipt.citation_blocks = citationBlocks;
797
+ // A verifier the machine cannot execute is a broken harness, not a verdict
798
+ // on the diff. It must never sit quietly in the skipped pile: surface it
799
+ // as an alarm so a wedged loop is visible on the first tick, not the 480th.
800
+ // Not filtered by action: an unrunnable check surfaces as `skipped` from
801
+ // the eligibility gate and as `revised`/`revise_failed` from the landing
802
+ // gate. The reason is the signal, not the lane it came through.
803
+ const unrunnable = results
804
+ .filter((r) => r && r.action !== 'accepted' && r.reason === 'verify_unrunnable')
805
+ .map((r) => ({
806
+ ref: r.ref,
807
+ cause: r.unrunnable_cause || 'unknown',
808
+ command: r.command || r.verify || null,
809
+ }));
810
+ if (unrunnable.length) {
811
+ receipt.unrunnable_verifiers = unrunnable;
812
+ receipt.alarms = Number(receipt.alarms || 0) + unrunnable.length;
813
+ }
737
814
  receipt.certified = parsed.certified ?? null;
738
815
  receipt.scanned = parsed.scanned ?? null;
739
816
  receipt.revised = parsed.revised ?? null;
@@ -956,7 +1033,12 @@ function runTickBody(root, { json, policy, receipt }) {
956
1033
  const heldNote = receipt.missions_held ? `, held ${receipt.missions_held} human-blocked mission${receipt.missions_held === 1 ? '' : 's'}` : '';
957
1034
  const wishNote = `, ${wishSweepSummaryLine(receipt.wish_dispatch, receipt.wish_dispatch_error)}`;
958
1035
  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}`;
1036
+ // Deltas alone ("0 certified, 0 landed") are true and useless: they read
1037
+ // as "nothing to do" whether the queue is empty or 19-deep and wedged.
1038
+ // Always print the standing backlog, and name work that cleared every
1039
+ // gate and still did not land — that is the shape of a stuck loop.
1040
+ const stuckNote = stuckBacklogNote(receipt);
1041
+ 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
1042
  console.log(gateForHuman(summary).text);
961
1043
  for (const fulfilled of receipt.wish_dispatch?.fulfilled_results || []) {
962
1044
  if (fulfilled.review_ask) console.log(fulfilled.review_ask);
@@ -1014,18 +1096,14 @@ function autolandCommand(args = []) {
1014
1096
 
1015
1097
  module.exports = {
1016
1098
  autolandCommand,
1017
- attachedTasksForMission,
1018
1099
  digestNextMoves,
1019
1100
  digestStoryRows,
1020
1101
  digestTickStatus,
1021
1102
  explainResult,
1022
- missionReadyForClosedTaskVerify,
1023
1103
  operatorReady,
1024
1104
  readAcceptedTaskHistory,
1025
1105
  hasAgentJargon,
1026
1106
  runTickBody,
1027
- persistTickReceipt,
1028
1107
  sweepLanding,
1029
1108
  verifyClosedTaskMissions,
1030
- wishSweepSummaryLine,
1031
1109
  };
@@ -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
package/commands/brain.js CHANGED
@@ -1714,17 +1714,10 @@ function brainCommand(args = process.argv.slice(3)) {
1714
1714
  module.exports = {
1715
1715
  brainCommand,
1716
1716
  collectState,
1717
- prepareBrainState,
1718
1717
  renderStatus,
1719
- renderLedger,
1720
- renderActivationCard,
1721
- renderActivationGallery,
1722
1718
  recordTaskEpisodeScorecards,
1723
- recordFeedback,
1724
- recordApproval,
1725
1719
  verifyActivationCard,
1726
1720
  verifyActivationGallery,
1727
- verifyBrain,
1728
1721
  parseContributionCard,
1729
1722
  escapeRegExp,
1730
1723
  countTodoItems,
package/commands/brief.js CHANGED
@@ -642,10 +642,8 @@ module.exports = {
642
642
  buildBriefData,
643
643
  briefCommand,
644
644
  handleBriefAnswer,
645
- parsePromptAnswer,
646
645
  renderBrief,
647
646
  renderBriefHtml,
648
647
  run: briefCommand,
649
- runBriefLedgerCommand,
650
648
  shouldPromptBrief,
651
649
  };
@@ -1082,7 +1082,6 @@ module.exports = {
1082
1082
  buildConflictMarkers,
1083
1083
  canPreviewPush,
1084
1084
  collectBrainSnapshot,
1085
- collectWorkspaceSnapshot,
1086
1085
  collectWorkspaceWarnings,
1087
1086
  collectLocalSyncStatus,
1088
1087
  collectConflictResolutionEntries,
@@ -6,6 +6,12 @@ const { apiRequestJson } = require('../utils/api');
6
6
  const { syncBusinessCanonical, ensureWorkspaceStateFiles, ensureBusinessRootAgentAdapters } = require('./sync');
7
7
  const { ensureContextScaffold, writeWikiStatus, appendWikiLog } = require('../lib/wiki');
8
8
  const { writeRuntimeReceipt } = require('../lib/runtime-bootstrap');
9
+ const {
10
+ SIMULATE_USAGE,
11
+ buildSimulationPlan,
12
+ parseSimulationArgs,
13
+ renderDryRun,
14
+ } = require('../lib/business-simulate');
9
15
 
10
16
  function getBusinessConfigPath() {
11
17
  const home = require('os').homedir();
@@ -2386,6 +2392,7 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
2386
2392
  options.here ||
2387
2393
  Boolean(options.root)
2388
2394
  );
2395
+ let workspaceRoot = null;
2389
2396
 
2390
2397
  // Scaffold legacy local directory if in an atris project
2391
2398
  const atrisDir = !shouldCreateCanonicalWorkspace ? findAtrisDir() : null;
@@ -2405,7 +2412,7 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
2405
2412
  console.log(` Local scaffold: ${bizDir}/`);
2406
2413
  }
2407
2414
  } else if (shouldCreateCanonicalWorkspace) {
2408
- const workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
2415
+ workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
2409
2416
  const scaffold = createCanonicalBusinessWorkspace(workspaceRoot, {
2410
2417
  business_id: biz.id,
2411
2418
  workspace_id: biz.workspace_id,
@@ -2446,10 +2453,10 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
2446
2453
  console.log(` Agent: ${biz.agent_id || '(none)'}`);
2447
2454
  console.log(` Dashboard: https://atris.ai/dashboard/gm/${biz.id}`);
2448
2455
  if (shouldCreateCanonicalWorkspace) {
2449
- const workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
2450
2456
  console.log(renderBusinessCreatedNextSteps(biz, workspaceRoot));
2451
2457
  }
2452
2458
  console.log('');
2459
+ return { business: biz, workspaceRoot };
2453
2460
  }
2454
2461
 
2455
2462
  async function createBusiness(name, ...flags) {
@@ -2875,6 +2882,96 @@ function findAtrisDir() {
2875
2882
  return null;
2876
2883
  }
2877
2884
 
2885
+ function printBusinessSimulateHelp() {
2886
+ console.log(SIMULATE_USAGE);
2887
+ console.log('');
2888
+ console.log('Creates a business workspace, four simulation members, their missions, and atris/ENDGAME.md.');
2889
+ console.log('Use --dry-run to preview every simulation file without cloud calls or writes.');
2890
+ }
2891
+
2892
+ async function simulateBusiness(args = [], dependencies = {}) {
2893
+ const options = parseSimulationArgs(args);
2894
+ const createdAt = new Date().toISOString();
2895
+ const preview = buildSimulationPlan({
2896
+ idea: options.idea,
2897
+ businessName: options.businessName,
2898
+ roleNames: options.roleNames,
2899
+ createdAt,
2900
+ });
2901
+
2902
+ if (options.dryRun) {
2903
+ console.log(renderDryRun(preview));
2904
+ return preview;
2905
+ }
2906
+
2907
+ const init = dependencies.initBusinessWorkspace || initBusinessWorkspace;
2908
+ const runMemberCommand = dependencies.memberCommand || require('./member').memberCommand;
2909
+ const initialized = await init(options.businessName);
2910
+ const business = initialized?.business || {};
2911
+ const workspaceRoot = initialized?.workspaceRoot
2912
+ || resolveWorkspaceRoot(business.slug || options.businessSlug, {});
2913
+ const plan = buildSimulationPlan({
2914
+ idea: options.idea,
2915
+ businessName: business.name || options.businessName,
2916
+ businessSlug: business.slug || options.businessSlug,
2917
+ roleNames: options.roleNames,
2918
+ createdAt,
2919
+ workspacePath: workspaceRoot,
2920
+ });
2921
+
2922
+ const businessJson = path.join(workspaceRoot, '.atris', 'business.json');
2923
+ if (!fs.existsSync(businessJson)) {
2924
+ throw new Error(`Business init did not create the expected workspace: ${workspaceRoot}`);
2925
+ }
2926
+
2927
+ const originalCwd = process.cwd();
2928
+ try {
2929
+ process.chdir(workspaceRoot);
2930
+
2931
+ const endgame = plan.files.find((file) => file.kind === 'endgame');
2932
+ fs.writeFileSync(path.join(workspaceRoot, endgame.path), endgame.content, 'utf8');
2933
+
2934
+ for (const role of plan.roles) {
2935
+ const memberDir = path.join(workspaceRoot, 'atris', 'team', role.name);
2936
+ const memberFile = path.join(memberDir, 'MEMBER.md');
2937
+ if (!fs.existsSync(memberFile)) {
2938
+ await runMemberCommand(
2939
+ 'create',
2940
+ role.name,
2941
+ `--role=${role.title}`,
2942
+ `--description=${role.description}`,
2943
+ );
2944
+ } else {
2945
+ console.log(`Using existing team/${role.name}/ scaffold from business init.`);
2946
+ }
2947
+
2948
+ for (const directory of ['skills', 'tools', 'context', 'logs']) {
2949
+ fs.mkdirSync(path.join(memberDir, directory), { recursive: true });
2950
+ }
2951
+
2952
+ const roleFiles = plan.files.filter((file) => file.role?.name === role.name);
2953
+ for (const file of roleFiles) {
2954
+ fs.writeFileSync(path.join(workspaceRoot, file.path), file.content, 'utf8');
2955
+ }
2956
+
2957
+ await runMemberCommand('push', role.name);
2958
+ }
2959
+ } finally {
2960
+ process.chdir(originalCwd);
2961
+ }
2962
+
2963
+ console.log('');
2964
+ console.log('business simulation ready');
2965
+ console.log(`workspace: ${workspaceRoot}`);
2966
+ console.log('roster:');
2967
+ for (const role of plan.roles) {
2968
+ console.log(` - ${role.name}: ${role.role}`);
2969
+ }
2970
+ console.log('next: open an agent session in the workspace and run the loop against atris/ENDGAME.md');
2971
+ console.log('');
2972
+ return plan;
2973
+ }
2974
+
2878
2975
 
2879
2976
  async function quickstart() {
2880
2977
  console.log(`
@@ -2945,6 +3042,7 @@ function printBusinessHelp() {
2945
3042
  console.log(' init <name> RECOMMENDED: create a business environment (cloud + local)');
2946
3043
  console.log(' workspace <name> Alias for init');
2947
3044
  console.log(' create <name> Cloud-only business record; add --workspace to also scaffold local');
3045
+ console.log(' simulate <idea> Create a business, four-role team, missions, and endgame loop');
2948
3046
  console.log(' add <slug> Register an existing cloud business');
2949
3047
  console.log(' list Show registered businesses');
2950
3048
  console.log(' team [slug] Show members, roles, and admin access');
@@ -2973,6 +3071,10 @@ async function businessCommand(subcommand, ...args) {
2973
3071
  printBusinessHelp();
2974
3072
  return;
2975
3073
  }
3074
+ if (subcommand === 'simulate' && args.some(isHelpToken)) {
3075
+ printBusinessSimulateHelp();
3076
+ return;
3077
+ }
2976
3078
  const nameTakingSubcommands = new Set(['create', 'new', 'init', 'workspace']);
2977
3079
  if (nameTakingSubcommands.has(subcommand) && args.some(isHelpToken)) {
2978
3080
  printBusinessHelp();
@@ -2995,6 +3097,9 @@ async function businessCommand(subcommand, ...args) {
2995
3097
  case 'workspace':
2996
3098
  await initBusinessWorkspace(args[0], ...args.slice(1));
2997
3099
  break;
3100
+ case 'simulate':
3101
+ await simulateBusiness(args);
3102
+ break;
2998
3103
  case 'list':
2999
3104
  case 'ls': {
3000
3105
  const opts = {};
@@ -3072,26 +3177,16 @@ async function businessCommand(subcommand, ...args) {
3072
3177
 
3073
3178
  module.exports = {
3074
3179
  businessCommand,
3075
- businessHealth,
3076
- businessAudit,
3077
- businessDoctor,
3078
- businessTeam,
3079
3180
  loadBusinesses,
3080
3181
  saveBusinesses,
3081
- getBusinessConfigPath,
3082
3182
  businessMatchesSlug,
3083
3183
  isAccidentalHelpBusiness,
3084
3184
  validateBusinessCreateName,
3085
3185
  analyzeBusinessDoctor,
3086
- readBusinessFolderBindings,
3087
3186
  createCanonicalBusinessWorkspace,
3088
- initBusinessWorkspace,
3089
3187
  onboardBusiness,
3090
- startBusinessWorkspace,
3091
3188
  shareBusinessWorkspace,
3092
3189
  collectBusinessShareState,
3093
3190
  renderBusinessCreatedNextSteps,
3094
- renderBusinessShareHandoff,
3095
- renderBusinessStartCard,
3096
3191
  recordBusinessRun,
3097
3192
  };
package/commands/clean.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const escapeRegExp = require('../lib/escape-regexp');
5
+ const { wikiMetabolismNudge } = require('../lib/wiki');
5
6
 
6
7
  /**
7
8
  * atris clean - Workspace housekeeping with auto-heal
@@ -60,6 +61,14 @@ function cleanAtris(options = {}) {
60
61
  const stalePages = findStalePages(cwd, atrisDir);
61
62
  results.stalePages = stalePages;
62
63
 
64
+ // 6. Dead code report (report-only: deletion is a human/reviewed call)
65
+ try {
66
+ const { findDeadCode } = require('./slop');
67
+ const dead = findDeadCode(cwd);
68
+ results.deadFiles = dead.dead.map((f) => path.relative(cwd, f));
69
+ results.testOnlyFiles = dead.testOnly.map((f) => path.relative(cwd, f));
70
+ } catch { results.deadFiles = []; results.testOnlyFiles = []; }
71
+
63
72
  if (options.json) {
64
73
  console.log(JSON.stringify(cleanResultPayload(results, options, cwd), null, 2));
65
74
  return results;
@@ -75,6 +84,12 @@ function cleanAtris(options = {}) {
75
84
  console.log('Results:');
76
85
  console.log('');
77
86
 
87
+ const wikiNudge = wikiMetabolismNudge(cwd);
88
+ if (wikiNudge) {
89
+ console.log(wikiNudge);
90
+ console.log('');
91
+ }
92
+
78
93
  // Stale tasks
79
94
  if (staleTasks.length > 0) {
80
95
  console.log(`⚠ ${staleTasks.length} stale ${staleTasks.length === 1 ? 'task' : 'tasks'} (claimed >3 days, not completed):`);
@@ -140,6 +155,16 @@ function cleanAtris(options = {}) {
140
155
  console.log('✓ No stale wiki pages');
141
156
  }
142
157
 
158
+ // Dead code
159
+ if ((results.deadFiles || []).length > 0) {
160
+ console.log(`⚠ ${results.deadFiles.length} dead ${results.deadFiles.length === 1 ? 'file' : 'files'} (unreachable and unreferenced — atris slop dead for detail):`);
161
+ results.deadFiles.slice(0, 5).forEach((f) => console.log(` • ${f}`));
162
+ if (results.deadFiles.length > 5) console.log(` ... and ${results.deadFiles.length - 5} more`);
163
+ console.log('');
164
+ } else {
165
+ console.log('✓ No dead code');
166
+ }
167
+
143
168
  const hasIssues = staleTasks.length > 0 || unhealable.length > 0 || stalePages.length > 0;
144
169
  if (hasIssues) {
145
170
  console.log('Manual action needed:');
@@ -165,6 +190,7 @@ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
165
190
  if (results.staleTasks.length > 0) manualAction.push('Delete stale tasks or finish them');
166
191
  if (results.unhealableRefs.length > 0) manualAction.push('Manually fix unhealable MAP.md refs');
167
192
  if (results.stalePages.length > 0) manualAction.push('Re-compile stale pages');
193
+ if ((results.deadFiles || []).length > 0) manualAction.push('Delete dead files (atris slop dead)');
168
194
  return {
169
195
  ok: manualAction.length === 0,
170
196
  action: 'clean',
@@ -196,6 +222,11 @@ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
196
222
  stale_source: page.staleSource,
197
223
  })),
198
224
  },
225
+ dead_code: {
226
+ count: (results.deadFiles || []).length,
227
+ items: results.deadFiles || [],
228
+ test_only: results.testOnlyFiles || [],
229
+ },
199
230
  },
200
231
  manual_action: manualAction,
201
232
  };
@@ -659,8 +690,6 @@ module.exports = {
659
690
  cleanResultPayload,
660
691
  findStaleTasks,
661
692
  healBrokenMapRefs,
662
- archiveOldJournals,
663
- cleanEmptySections,
664
693
  findStalePages,
665
694
  checkPageStaleness
666
695
  };
package/commands/close.js CHANGED
@@ -517,14 +517,27 @@ function pulseLivenessState(cwd, now) {
517
517
  // pulse liveness check only sees silence; this sees a run of negative reward.
518
518
  // Sums reward over the last N finished ticks; unhealthy when the trend is net
519
519
  // negative, healthy once it climbs back to zero or better.
520
+ //
521
+ // Recency guard: this sensor only speaks to a *live* run of ticks. A
522
+ // decommissioned or intermittent loop leaves stale receipts frozen; without a
523
+ // guard the last N finished ticks stay net-negative forever and the flag can
524
+ // never clear (no fresh positive receipt arrives). It also must not treat a
525
+ // scattered set — e.g. one tick today plus five from days ago — as a "run":
526
+ // summing rewards across a multi-day gap is meaningless. So we only consider
527
+ // finished ticks from the last few days and require a full window of them; a
528
+ // silent or sparse loop is the pulse-liveness sensor's concern, and here it
529
+ // reports healthy so any open reward flag auto-closes.
520
530
  const REWARD_REGRESSION_WINDOW = 6;
531
+ const REWARD_REGRESSION_RECENCY_MS = 2 * DAY_MS;
521
532
 
522
533
  function rewardRegressionState(cwd, now) {
523
534
  const receipts = pulse.readJsonl(pulse.pulseReceiptsPath(cwd));
524
535
  const finished = receipts
525
536
  .filter((r) => r && r.phase === 'finished' && parseDate(r.ts || r.at) !== null)
537
+ .filter((r) => now.getTime() - parseDate(r.ts || r.at) <= REWARD_REGRESSION_RECENCY_MS)
526
538
  .sort((a, b) => parseDate(a.ts || a.at) - parseDate(b.ts || b.at));
527
- if (finished.length < REWARD_REGRESSION_WINDOW) return { healthy: false, unhealthy: false };
539
+ // Not enough recent signal to call a degradation run: leave it to pulse liveness.
540
+ if (finished.length < REWARD_REGRESSION_WINDOW) return { healthy: true, unhealthy: false, sparse: true };
528
541
  const window = finished.slice(-REWARD_REGRESSION_WINDOW);
529
542
  const sum = window.reduce((acc, r) => acc + (Number(r.reward) || 0), 0);
530
543
  return { healthy: sum >= 0, unhealthy: sum < 0, sum };
@@ -782,13 +795,14 @@ function appendOpenedFromSource({ what, owner = 'operator', lane = 'code', ttlDa
782
795
  return event;
783
796
  }
784
797
 
785
- function appendClosedFromSource(flag, cwd, now) {
798
+ function appendClosedFromSource(flag, cwd, now, note) {
786
799
  const event = {
787
800
  kind: 'closed',
788
801
  at: now.toISOString(),
789
802
  id: flag.id,
790
803
  proof: RESOLVED_IN_SOURCE_PROOF,
791
804
  };
805
+ if (note) event.note = note;
792
806
  appendEvent(event, cwd);
793
807
  return event;
794
808
  }
@@ -809,12 +823,20 @@ function scanState(cwd = process.cwd(), options = {}) {
809
823
  const lessonFlags = lessonFlagsState(cwd, now);
810
824
  const openBySource = new Map();
811
825
  const dissolvedSources = new Set();
826
+ const recentlyClosedSources = new Set();
812
827
  const autoClosed = [];
813
828
 
814
829
  // Dissolve is a durable verdict: a source the operator declared dead on
815
- // purpose must never re-open on a later scan.
830
+ // purpose must never re-open on a later scan. Recent closes get a 30-day
831
+ // quiet period so a resolved source cannot immediately breed a new flag.
816
832
  for (const flag of foldEvents(readEvents(cwd), { now })) {
817
833
  if (flag.status === 'dissolved' && flag.source) dissolvedSources.add(flag.source);
834
+ const closedAt = parseDate(flag.closed_at);
835
+ const closedAge = closedAt === null ? null : now.getTime() - closedAt;
836
+ if (flag.status === 'closed' && flag.source && closedAge !== null
837
+ && closedAge >= 0 && closedAge <= 30 * DAY_MS) {
838
+ recentlyClosedSources.add(flag.source);
839
+ }
818
840
  }
819
841
 
820
842
  const failedTasks = Array.from(tasksById.values())
@@ -827,17 +849,22 @@ function scanState(cwd = process.cwd(), options = {}) {
827
849
  if (flag.source) openBySource.set(flag.source, flag);
828
850
  }
829
851
 
830
- const closeFlag = (flag) => {
831
- const event = appendClosedFromSource(flag, cwd, now);
852
+ const closeFlag = (flag, note) => {
853
+ const event = appendClosedFromSource(flag, cwd, now, note);
832
854
  autoClosed.push({ flag, event });
833
855
  openBySource.delete(flag.source);
856
+ recentlyClosedSources.add(flag.source);
834
857
  };
835
858
 
836
859
  for (const flag of Array.from(openBySource.values())) {
837
860
  if (flag.source.startsWith('task:')) {
838
861
  const task = tasksById.get(flag.source.slice('task:'.length));
839
862
  const status = normalizeSpaces(task && task.status).toLowerCase();
840
- if (task && (taskResolved(task) || status === 'failed')) closeFlag(flag);
863
+ if (!task) {
864
+ closeFlag(flag, 'task no longer in projection (done/archived)');
865
+ } else if (!taskWaitingOnHumanAccept(task) || taskResolved(task) || status === 'failed') {
866
+ closeFlag(flag);
867
+ }
841
868
  continue;
842
869
  }
843
870
  if (flag.source.startsWith(FAILED_TASK_BATCH_PREFIX)) {
@@ -881,7 +908,8 @@ function scanState(cwd = process.cwd(), options = {}) {
881
908
 
882
909
  const opened = [];
883
910
  const openCandidate = (candidate) => {
884
- if (!candidate.source || openBySource.has(candidate.source) || dissolvedSources.has(candidate.source)) return null;
911
+ if (!candidate.source || openBySource.has(candidate.source)
912
+ || dissolvedSources.has(candidate.source) || recentlyClosedSources.has(candidate.source)) return null;
885
913
  const event = appendOpenedFromSource(candidate, cwd, now);
886
914
  opened.push(event);
887
915
  openBySource.set(candidate.source, { ...event, status: 'open' });
@@ -889,10 +917,11 @@ function scanState(cwd = process.cwd(), options = {}) {
889
917
  };
890
918
 
891
919
  for (const task of tasks) {
892
- const source = taskSource(task);
893
- const ref = taskRef(task);
920
+ const currentTask = tasksById.get(normalizeSpaces(task && task.id));
921
+ const source = taskSource(currentTask);
922
+ const ref = taskRef(currentTask);
894
923
  if (!source || !ref) continue;
895
- if (taskWaitingOnHumanAccept(task) && isOlderThan(task.updated_at, 5, now)) {
924
+ if (taskWaitingOnHumanAccept(currentTask) && isOlderThan(currentTask.updated_at, 5, now)) {
896
925
  openCandidate({
897
926
  what: `task ${ref} has waited in review too long, accept it or send it back`,
898
927
  lane: 'code',
@@ -1054,6 +1083,13 @@ function run(args = [], context = {}) {
1054
1083
  printHelp();
1055
1084
  return 0;
1056
1085
  }
1086
+ // `close <sub> --help` is a help request, not a run — sweep and scan mutate
1087
+ // the ledger (escalations, auto-closes), so never execute a subcommand just
1088
+ // to show usage.
1089
+ if (rest.includes('--help') || rest.includes('-h')) {
1090
+ printHelp();
1091
+ return 0;
1092
+ }
1057
1093
  if (subcommand === 'add') return commandAdd(rest, context);
1058
1094
  if (subcommand === 'list') return commandList(rest, context);
1059
1095
  if (subcommand === 'done') return commandDone(rest, context);
@@ -1078,6 +1114,5 @@ module.exports = {
1078
1114
  openFlags,
1079
1115
  ledgerPath,
1080
1116
  readEvents,
1081
- listLine,
1082
1117
  sweepLine,
1083
1118
  };
package/commands/cloud.js CHANGED
@@ -240,6 +240,5 @@ module.exports = {
240
240
  cloudClean,
241
241
  collectCloudOrphans,
242
242
  parseCloudCleanArgs,
243
- readCloudCleanSlug,
244
243
  renderCloudCleanSummary,
245
244
  };