atris 3.58.6 → 3.58.8

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 (90) hide show
  1. package/GETTING_STARTED.md +2 -2
  2. package/PERSONA.md +11 -11
  3. package/README.md +9 -1
  4. package/atris/GEMINI.md +7 -6
  5. package/atris/PERSONA.md +4 -4
  6. package/atris/features/README.md +77 -6
  7. package/atris/policies/engineering-principles.md +129 -0
  8. package/atris/policies/genesis.md +112 -0
  9. package/atris/policies/product-design-principles.md +100 -0
  10. package/atris/skills/README.md +44 -15
  11. package/atris/skills/aeo/SKILL.md +10 -10
  12. package/atris/skills/atris/SKILL.md +1 -1
  13. package/atris/skills/design/SKILL.md +3 -1
  14. package/atris/skills/engines/SKILL.md +42 -50
  15. package/atris/skills/meta/SKILL.md +4 -4
  16. package/atris/skills/youtube/SKILL.md +11 -11
  17. package/bin/atris.js +60 -8
  18. package/commands/aeo.js +9 -12
  19. package/commands/analytics.js +8 -11
  20. package/commands/auth.js +8 -2
  21. package/commands/autopilot.js +4 -3
  22. package/commands/avail.js +1 -1
  23. package/commands/brain.js +4 -1
  24. package/commands/business.js +13 -8
  25. package/commands/close.js +2 -1
  26. package/commands/computer.js +15 -10
  27. package/commands/context-sync.js +3 -3
  28. package/commands/design.js +362 -0
  29. package/commands/doc-health.js +377 -0
  30. package/commands/drive.js +32 -0
  31. package/commands/experiments.js +13 -4
  32. package/commands/improve.js +68 -2
  33. package/commands/integrations.js +2 -5
  34. package/commands/land.js +144 -4
  35. package/commands/learn.js +7 -2
  36. package/commands/member.js +239 -58
  37. package/commands/mission.js +94 -18
  38. package/commands/pack.js +2 -2
  39. package/commands/probe.js +11 -5
  40. package/commands/pull.js +1 -1
  41. package/commands/pulse.js +38 -0
  42. package/commands/push.js +1 -1
  43. package/commands/rainmaker.js +3 -16
  44. package/commands/rsi.js +156 -0
  45. package/commands/spaceship.js +1 -1
  46. package/commands/stream.js +27 -2
  47. package/commands/study.js +15 -9
  48. package/commands/sync.js +1 -1
  49. package/commands/task.js +54 -3
  50. package/commands/workflow.js +17 -7
  51. package/commands/youtube.js +76 -8
  52. package/lib/apply-gate.js +22 -4
  53. package/lib/codex-flight.js +28 -5
  54. package/lib/daily-log.js +88 -0
  55. package/lib/design-api.js +130 -0
  56. package/lib/engine-ask.js +1 -1
  57. package/lib/first-minute.js +1 -6
  58. package/lib/fleet.js +4 -1
  59. package/lib/improve-vitals-html.js +3 -2
  60. package/lib/known-commands.js +3 -3
  61. package/lib/lesson-contradiction.js +2 -2
  62. package/lib/map-refs.js +183 -0
  63. package/lib/member-context.js +42 -0
  64. package/lib/mission-room.js +4 -4
  65. package/lib/noninteractive.js +15 -4
  66. package/lib/policy-lessons.js +1 -1
  67. package/lib/receipt-block.js +137 -28
  68. package/lib/rsi-record.js +335 -0
  69. package/lib/runner-command.js +1 -1
  70. package/lib/scorecard.js +4 -4
  71. package/lib/sleep-sync.js +11 -0
  72. package/lib/state-detection.js +36 -17
  73. package/lib/task-db.js +74 -53
  74. package/lib/task-list-keeper.js +192 -0
  75. package/lib/todo-fallback.js +10 -4
  76. package/lib/todo.js +22 -10
  77. package/lib/wish-delegate.js +19 -5
  78. package/mcp/atris-mcp/index.mjs +227 -0
  79. package/package.json +4 -2
  80. package/scripts/det/det.js +2 -2
  81. package/scripts/det/pr-description.js +2 -2
  82. package/scripts/det/test.js +2 -2
  83. package/scripts/det/ytnotes +43 -8
  84. package/utils/api.js +1 -1
  85. package/utils/auth.js +79 -14
  86. package/utils/backend-root.js +31 -0
  87. package/utils/config.js +5 -4
  88. package/utils/owner-identity.js +61 -0
  89. package/atris/policies/LESSONS.md +0 -13
  90. package/atris/skills/autopilot/atris-autopilot.md +0 -71
@@ -6,12 +6,14 @@ const crypto = require('crypto');
6
6
  const readline = require('readline');
7
7
  const { spawn, spawnSync } = require('child_process');
8
8
  const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
9
+ const { memberProcessPrompt } = require('../lib/member-context');
9
10
  const {
10
11
  compactErrorPayload,
11
12
  compactSuccessPayload,
12
13
  printCliJson,
13
14
  } = require('../lib/cli-json');
14
15
  const escapeRegExp = require('../lib/escape-regexp');
16
+ const { sleepSync } = require('../lib/sleep-sync');
15
17
  const {
16
18
  appendBriefRecord,
17
19
  stampBriefOutcome,
@@ -111,6 +113,15 @@ const CODEX_NATIVE_GOAL_CLOSED_STATUSES = new Set(['complete', 'completed', 'ach
111
113
  const DEFAULT_LONG_RUN_VERIFIER = 'git diff --check';
112
114
  const SLEEP_LENGTH_BUDGET_SECONDS = 3600;
113
115
  const HUMAN_BLOCKING_PAUSE_REASONS = new Set(['auth-required', 'model-unavailable', 'rate-limit-exceeded-wall']);
116
+ // Claude prints its logged-out message on stdout ("Invalid API key · Please
117
+ // run /login"), not stderr, so the check has to read the captured result text
118
+ // too or a dead login masquerades as a generic claude-error.
119
+ const MISSION_RUNNER_AUTH_EXPIRED_RE = /not authenticated|please log in|login required|auth(?:entication)? expired|invalid (?:api|connection) key|run \/login/i;
120
+
121
+ function missionRunnerAuthExpired(...texts) {
122
+ return texts.some((text) => Boolean(text) && MISSION_RUNNER_AUTH_EXPIRED_RE.test(String(text)));
123
+ }
124
+
114
125
  const MISSION_BUDGET_TIERS = Object.freeze({
115
126
  quick: Object.freeze({ max_ticks: 4, requested_seconds: 15 * 60 }),
116
127
  long: Object.freeze({ max_ticks: 12, requested_seconds: 60 * 60 }),
@@ -3558,8 +3569,8 @@ function readTasteReviewHistory(root, limit = 4) {
3558
3569
 
3559
3570
  function missionTasteMemorySignals(text) {
3560
3571
  const definitions = [
3561
- ['plain_language', 'Keshav asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
3562
- ['proof_gate', 'Keshav wants proof before accept', /\b(proof|receipt|verify|verified|checked|tested|accept|approval)\b/i],
3572
+ ['plain_language', 'the operator asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
3573
+ ['proof_gate', 'the operator wants proof before accept', /\b(proof|receipt|verify|verified|checked|tested|accept|approval)\b/i],
3563
3574
  ['runway_revenue', 'runway pushes user or revenue work', /\b(runway|revenue|cash|user|users|customer|demo|adoption|retention)\b/i],
3564
3575
  ['technical_ambition', 'technical advancement still matters', /\b(technical|homerun|research|runtime|architecture|model|benchmark|agent)\b/i],
3565
3576
  ['trust_boundary', 'trust and approval boundaries matter', /\b(security|safe|safety|trust|privacy|approval|permission|gmail|email|connector)\b/i],
@@ -3658,9 +3669,9 @@ function missionPlainTaskPreview(title) {
3658
3669
  function missionTasteMemoryReason(tasteMemory, signals) {
3659
3670
  const candidateIds = new Set((signals || []).map((signal) => signal.id));
3660
3671
  const memoryIds = new Set((tasteMemory?.signals || []).map((signal) => signal.id));
3661
- if (memoryIds.has('runway_revenue') && candidateIds.has('users_revenue')) return 'Keshav has been steering toward user/revenue proof because runway is tight.';
3662
- if (memoryIds.has('plain_language') && candidateIds.has('usability')) return 'Keshav keeps asking for plain-English, understandable work.';
3663
- if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return 'Keshav wants proof and receipts before serious work is accepted.';
3672
+ if (memoryIds.has('runway_revenue') && candidateIds.has('users_revenue')) return 'the operator has been steering toward user/revenue proof because runway is tight.';
3673
+ if (memoryIds.has('plain_language') && candidateIds.has('usability')) return 'the operator keeps asking for plain-English, understandable work.';
3674
+ if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return 'the operator wants proof and receipts before serious work is accepted.';
3664
3675
  if (memoryIds.has('working_memory')) return 'Recent logs are being used as working memory for what matters now.';
3665
3676
  if (memoryIds.has('technical_ambition') && candidateIds.has('technical')) return 'The member memory still values technical advancement when it has proof.';
3666
3677
  return '';
@@ -4150,14 +4161,61 @@ function startMission(args, options = {}) {
4150
4161
  // checkout's dirt never reaches the mission baseline.
4151
4162
  if (hasFlag(args, '--worktree')) {
4152
4163
  let created;
4164
+ const sourceTop = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8' });
4165
+ const sourceCheckout = sourceTop.status === 0 ? String(sourceTop.stdout).trim() : process.cwd();
4166
+ let nodeModulesSource = null;
4153
4167
  try {
4154
4168
  const { createAgentWorktree } = require('./worktree');
4155
4169
  const base = readFlag(args, '--base', '') || inheritedWorktreeBase(process.cwd());
4156
4170
  created = createAgentWorktree({ member: mission.owner, task: mission.objective, ...(base ? { base } : {}) });
4171
+ const packageFile = path.join(sourceCheckout, 'package.json');
4172
+ const sourceModules = path.join(sourceCheckout, 'node_modules');
4173
+ const targetModules = path.join(created.path, 'node_modules');
4174
+ try {
4175
+ let targetExists = false;
4176
+ try {
4177
+ fs.lstatSync(targetModules);
4178
+ targetExists = true;
4179
+ } catch (error) {
4180
+ if (error.code !== 'ENOENT') throw error;
4181
+ }
4182
+ if (fs.existsSync(packageFile) && fs.existsSync(sourceModules) && !targetExists) {
4183
+ const manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
4184
+ const hasDependencies = ['dependencies', 'devDependencies', 'optionalDependencies']
4185
+ .some((field) => manifest[field] && Object.keys(manifest[field]).length > 0);
4186
+ if (hasDependencies) {
4187
+ const checkIgnored = () => {
4188
+ const result = spawnSync('git', ['check-ignore', '-q', 'node_modules'], { cwd: created.path, encoding: 'utf8' });
4189
+ if (result.error || (result.status !== 0 && result.status !== 1)) {
4190
+ throw new Error(result.error?.message || String(result.stderr || 'git check-ignore failed').trim());
4191
+ }
4192
+ return result.status === 0;
4193
+ };
4194
+ if (!checkIgnored()) {
4195
+ const commonDir = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: created.path, encoding: 'utf8' });
4196
+ if (commonDir.error || commonDir.status !== 0) {
4197
+ throw new Error(commonDir.error?.message || String(commonDir.stderr || 'could not find shared git directory').trim());
4198
+ }
4199
+ const excludeFile = path.resolve(created.path, String(commonDir.stdout).trim(), 'info', 'exclude');
4200
+ fs.mkdirSync(path.dirname(excludeFile), { recursive: true });
4201
+ const exclude = fs.existsSync(excludeFile) ? fs.readFileSync(excludeFile, 'utf8') : '';
4202
+ if (!exclude.split(/\r?\n/).includes('/node_modules')) {
4203
+ fs.appendFileSync(excludeFile, `${exclude && !exclude.endsWith('\n') ? '\n' : ''}/node_modules\n`);
4204
+ }
4205
+ }
4206
+ if (!checkIgnored()) throw new Error('git still tracks node_modules after updating the shared exclude file');
4207
+ fs.symlinkSync(sourceModules, targetModules, 'dir');
4208
+ nodeModulesSource = sourceModules;
4209
+ }
4210
+ }
4211
+ } catch (error) {
4212
+ console.warn(`warning: could not link node_modules: ${String(error.message).split(/\r?\n/)[0]}`);
4213
+ }
4157
4214
  } catch (e) {
4158
4215
  exitMissionError(`[mission start] worktree creation failed: ${e.message}`, 2, asJson);
4159
4216
  }
4160
4217
  mission.worktree = { path: created.path, branch: created.branch, base: created.base };
4218
+ if (nodeModulesSource) mission.node_modules_source = nodeModulesSource;
4161
4219
  process.chdir(created.path);
4162
4220
  }
4163
4221
  if (mission.xp_task_enabled) {
@@ -4215,6 +4273,8 @@ function startMission(args, options = {}) {
4215
4273
  dirty_count: worktreeBaseline.dirty_count,
4216
4274
  dirty_hash: worktreeBaseline.dirty_hash,
4217
4275
  } : null,
4276
+ node_modules_linked: Boolean(saved.node_modules_source),
4277
+ node_modules_source: saved.node_modules_source || null,
4218
4278
  ...(saved.remap_reason ? { remap_reason: saved.remap_reason } : {}),
4219
4279
  };
4220
4280
  if (!options.silent) {
@@ -4226,6 +4286,7 @@ function startMission(args, options = {}) {
4226
4286
  `State: ${saved.status}`,
4227
4287
  ...(saved.remap_reason ? [`Remap: ${saved.remap_reason}`] : []),
4228
4288
  ...(saved.worktree ? [`Worktree: ${saved.worktree.path}`, `Branch: ${saved.worktree.branch}`] : []),
4289
+ ...(saved.node_modules_source ? [`linked node_modules from ${saved.node_modules_source}`] : []),
4229
4290
  ...warnings.map((warning) => `Warning: ${warning.message}`),
4230
4291
  ...(saved.xp_task ? [`AgentXP task: ${saved.xp_task.ref}`] : []),
4231
4292
  ...(saved.worktree ? [`Next: cd ${saved.worktree.path} && ${nextTickCommand}`] : [`Next: ${nextTickCommand}`]),
@@ -8039,12 +8100,6 @@ function isWithinActiveHours(activeHours, now = new Date()) {
8039
8100
  return cur >= start || cur < end;
8040
8101
  }
8041
8102
 
8042
- function sleepSync(ms) {
8043
- const waitMs = Math.max(0, Number(ms) || 0);
8044
- if (!waitMs) return;
8045
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
8046
- }
8047
-
8048
8103
  function missionLockOwnerIsAlive(pid) {
8049
8104
  const ownerPid = Number(pid);
8050
8105
  if (!Number.isInteger(ownerPid) || ownerPid <= 0) return false;
@@ -8406,8 +8461,12 @@ function probeClaudeBinary() {
8406
8461
  // Pull unread operator pings off the mission and mark them consumed, so the
8407
8462
  // next tick's prompt carries them exactly once. Pings are how a human talks to
8408
8463
  // an always-on member mid-run: atris member ping <name> "<msg>".
8464
+ function pendingMissionPings(mission) {
8465
+ return (Array.isArray(mission.pings) ? mission.pings : []).filter((p) => p && !p.consumed_at);
8466
+ }
8467
+
8409
8468
  function consumeMissionPings(mission, cwd) {
8410
- const pending = (Array.isArray(mission.pings) ? mission.pings : []).filter((p) => p && !p.consumed_at);
8469
+ const pending = pendingMissionPings(mission);
8411
8470
  if (!pending.length) return { mission, pings: [] };
8412
8471
  const consumedAt = stampIso();
8413
8472
  const pings = (mission.pings || []).map((p) => (p && !p.consumed_at ? { ...p, consumed_at: consumedAt } : p));
@@ -8415,7 +8474,8 @@ function consumeMissionPings(mission, cwd) {
8415
8474
  return { mission: saved, pings: pending };
8416
8475
  }
8417
8476
 
8418
- function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
8477
+ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd = process.cwd()) {
8478
+ const sharedProcess = memberProcessPrompt(cwd);
8419
8479
  const pingLines = pings.length
8420
8480
  ? [
8421
8481
  ``,
@@ -8447,6 +8507,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
8447
8507
  `**Last tick:** ${mission.last_tick_at || 'never'}`,
8448
8508
  ...missionBudgetPromptLines(mission),
8449
8509
  ...checkFeedbackLines,
8510
+ ...(sharedProcess ? ['', sharedProcess] : []),
8450
8511
  ``,
8451
8512
  `## Your task`,
8452
8513
  `Do ONE increment of work toward the stop condition. ONE. No more.`,
@@ -8765,7 +8826,7 @@ function spawnGenericRunnerTick(mission, opts) {
8765
8826
  const finalText = String(stdout || '').trim();
8766
8827
  const errStr = String(stderr || '').slice(-2000);
8767
8828
  const ok = code === 0 && !timedOut && !aborted;
8768
- const authExpired = /not authenticated|please log in|login required|auth(?:entication)? expired/i.test(errStr);
8829
+ const authExpired = !ok && missionRunnerAuthExpired(errStr, finalText);
8769
8830
  resolve({
8770
8831
  ok,
8771
8832
  brief_id: briefId,
@@ -8940,7 +9001,7 @@ function spawnClaudeTick(mission, opts) {
8940
9001
  updateMissionLockOwner(missionLock, missionLock?.driverPid);
8941
9002
  const ok = code === 0 && !isError && !timedOut && !aborted;
8942
9003
  const errStr = stderr.slice(-2000);
8943
- const authExpired = /not authenticated|please log in|login required|auth(?:entication)? expired/i.test(errStr);
9004
+ const authExpired = !ok && missionRunnerAuthExpired(errStr, finalText, stdoutBuf);
8944
9005
  resolve({
8945
9006
  ok,
8946
9007
  timedOut,
@@ -9521,6 +9582,9 @@ async function executeMissionRunTicksPhase(context) {
9521
9582
  drill: drillResult,
9522
9583
  };
9523
9584
  } else if (tickAtris2Runner) {
9585
+ // Build before consuming direction or allocating the guard: a shared
9586
+ // process read error must leave operator pings available for retry.
9587
+ const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pendingMissionPings(mission), cwd);
9524
9588
  let atris2GitGuard = null;
9525
9589
  try {
9526
9590
  atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
@@ -9541,7 +9605,6 @@ async function executeMissionRunTicksPhase(context) {
9541
9605
  const pingDrain = consumeMissionPings(mission, cwd);
9542
9606
  mission = pingDrain.mission;
9543
9607
  runtimeMission = runtimeView(mission);
9544
- const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
9545
9608
  const { runAtris2Turn } = require('./probe');
9546
9609
  const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
9547
9610
  const tickController = new AbortController();
@@ -9593,12 +9656,12 @@ async function executeMissionRunTicksPhase(context) {
9593
9656
  }
9594
9657
  }
9595
9658
  } else {
9659
+ const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pendingMissionPings(mission), cwd);
9596
9660
  let sessionMode = sessionId ? 'resume' : 'set';
9597
9661
  let useId = sessionId || pendingSessionId;
9598
9662
  const pingDrain = consumeMissionPings(mission, cwd);
9599
9663
  mission = pingDrain.mission;
9600
9664
  runtimeMission = runtimeView(mission);
9601
- const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
9602
9665
  const restoreTickRunnerProfile = tickEngineId ? applyMissionRunnerProfile(tickEngineId) : () => {};
9603
9666
  let claudeResult;
9604
9667
  let sessionBusyRetried = false;
@@ -9813,6 +9876,14 @@ async function executeMissionRunTicksPhase(context) {
9813
9876
  const claudeRanTick = Boolean(result.claude) && result.status === 'ran';
9814
9877
  const claudeSessionTicks = claudeRanTick ? Number(mission.claude_session_ticks || 0) + 1 : Number(mission.claude_session_ticks || 0);
9815
9878
  const rotateSessionForContext = claudeRanTick && claudeSessionTicks >= CLAUDE_SESSION_CONTEXT_ROTATE_TICKS;
9879
+ // Heartbeat missions run one tick per invocation, so the in-run ticks
9880
+ // array alone can never see two identical errors. Persist the trailing
9881
+ // same-reason errored count so the breaker below trips across runs.
9882
+ const errorStreakCount = result.status === 'errored' && result.reason
9883
+ ? (latestOnDisk.last_tick_status === 'errored' && latestOnDisk.last_tick_reason === result.reason
9884
+ ? Number(latestOnDisk.error_streak_count || 0) + 1
9885
+ : 1)
9886
+ : 0;
9816
9887
  // Base on latestOnDisk so mid-tick complete proof/completed_at survive.
9817
9888
  mission = saveMission({
9818
9889
  ...latestOnDisk,
@@ -9825,6 +9896,7 @@ async function executeMissionRunTicksPhase(context) {
9825
9896
  last_tick_index: tickIdx,
9826
9897
  last_tick_layer: result.layer,
9827
9898
  last_tick_layer_source: result.layer_source,
9899
+ error_streak_count: errorStreakCount,
9828
9900
  verifier_result: verifierResult || (verifyEach && result.protected_lane_guard && result.protected_lane_guard.allowed === false ? null : latestOnDisk.verifier_result) || null,
9829
9901
  last_check_feedback: verifierResult
9830
9902
  ? extractCheckFeedback(verifierResult)
@@ -9902,9 +9974,12 @@ async function executeMissionRunTicksPhase(context) {
9902
9974
  // is the same trap one step less deterministic: keep retrying and the loop burns every
9903
9975
  // tick + cron firing on it. Halt at two-in-a-row and surface the reason for a human.
9904
9976
  const errStreak = consecutiveSameReasonErrors(ticks);
9977
+ // The in-run ticks array only sees this invocation; the persisted streak
9978
+ // carries identical errors across heartbeat runs of one tick each.
9979
+ const errStreakCount = Math.max(errStreak.count, errStreak.reason ? Number(mission.error_streak_count || 0) : 0);
9905
9980
  // Sleeping Atris2 backends are different: leave the mission running so the
9906
9981
  // next tick or heartbeat can catch the backend after it wakes.
9907
- if (errStreak.count >= 2 && !missionRunKeepsRetryingError(errStreak.reason)) { pauseReason = `repeated-error:${errStreak.reason}`; break; }
9982
+ if (errStreakCount >= 2 && !missionRunKeepsRetryingError(errStreak.reason)) { pauseReason = `repeated-error:${errStreak.reason}`; break; }
9908
9983
 
9909
9984
  // Sleep until next tick
9910
9985
  let sleepMs = 0;
@@ -11599,6 +11674,7 @@ module.exports = {
11599
11674
  missionProtectedLaneHold,
11600
11675
  missionCommand,
11601
11676
  startMission,
11677
+ resolveMission,
11602
11678
  spawnMissionDriver,
11603
11679
  missionRunnerNeedsLiveSession,
11604
11680
  completeMission,
package/commands/pack.js CHANGED
@@ -1622,7 +1622,7 @@ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
1622
1622
  }
1623
1623
 
1624
1624
  // pack.json belongs to the pack, not to whatever repo happens to contain it
1625
- // (2026-07-27: publishing ~/arena/atrisos-backend/atris wrote a pack.json
1625
+ // (2026-07-27: publishing a backend workspace's atris folder wrote a pack.json
1626
1626
  // into atrisos-backend/ and polluted the source repo).
1627
1627
  const manifestPath = path.join(sourceDir, 'pack.json');
1628
1628
  const existing = readJson(manifestPath);
@@ -2453,7 +2453,7 @@ function printCapabilityTrustCard(policy, trust, receiptPath, userDenyRuleCount
2453
2453
  if (options.operatorInput) {
2454
2454
  console.log(` operator input: ${options.operatorInput.bytes} bytes injected; source path withheld from the pack and receipt`);
2455
2455
  }
2456
- console.log(` host shell: ${policy.grantedCapabilities.includes('host.shell') ? 'GRANTED \u2014 Bash can reach host files and network' : 'denied'}`);
2456
+ console.log(` host shell: ${policy.grantedCapabilities.includes('host.shell') ? 'granted, bash can reach host files and network' : 'denied'}`);
2457
2457
  console.log(' receipt coverage: Atris hook tool events; direct slash-skill invocations and later Claude or policy denials may not appear');
2458
2458
  console.log(` receipt: ${receiptPath}`);
2459
2459
  }
package/commands/probe.js CHANGED
@@ -454,18 +454,24 @@ async function probeCommand(argv) {
454
454
  const detail = ok
455
455
  ? `answer: ${resultText.slice(0, 80).replace(/\n/g, ' ')}`
456
456
  : `dead-end: ${deadEnd}`;
457
- const line = `- **atris-probe** \`${stamp}\` \u2014 ${where} · ${a.model}`
457
+ const line = buildProbeLine({ stamp, where, model: a.model, member, engine, ok, secs, toolsRun, calendar: a.calendar, cliCalendarOps, detail });
458
+ console.log(line);
459
+ return ok ? 0 : 1;
460
+ }
461
+
462
+ function buildProbeLine({ stamp, where, model, member, engine, ok, secs, toolsRun, calendar, cliCalendarOps, detail }) {
463
+ const line = `- **atris-probe** \`${stamp}\` - ${where} · ${model}`
458
464
  + (member ? ` · member=${member}` : '')
459
465
  + (engine ? ` · engine=${engine}` : '')
460
- + ` · ${ok ? 'PASS' : 'FAIL'} · ${secs}s · tools=${toolsRun}`
461
- + (a.calendar ? ` cal_cli=${cliCalendarOps}` : '')
466
+ + ` · ${ok ? 'pass' : 'fail'} · ${secs}s · tools=${toolsRun}`
467
+ + (calendar ? ` cal_cli=${cliCalendarOps}` : '')
462
468
  + ` · ${detail}`;
463
- console.log(line);
464
- return ok ? 0 : 1;
469
+ return line.replace(/\s*\u2014\s*/g, ', ');
465
470
  }
466
471
 
467
472
  module.exports = {
468
473
  probeCommand,
474
+ buildProbeLine,
469
475
  runAtris2Turn,
470
476
  // exported for tests
471
477
  normalizePath,
package/commands/pull.js CHANGED
@@ -722,7 +722,7 @@ async function pullBusiness(slug) {
722
722
  fs.mkdirSync(path.dirname(localPath), { recursive: true });
723
723
  fs.writeFileSync(localPath, content);
724
724
  }
725
- console.log(` \u26A0 ${p.replace(/^\//, '')} CONFLICT \u2014 both you and the computer changed this`);
725
+ console.log(` \u26A0 ${p.replace(/^\//, '')}: conflict, both you and the computer changed this`);
726
726
  console.log(` \u2192 Remote version saved as ${p.replace(/^\//, '')}.remote`);
727
727
  conflictCount++;
728
728
  }
package/commands/pulse.js CHANGED
@@ -18,6 +18,7 @@ const os = require('os');
18
18
  const path = require('path');
19
19
  const { spawnSync } = require('child_process');
20
20
  const pulse = require('../lib/pulse');
21
+ const rsi = require('../lib/rsi-record');
21
22
  const { DEFAULT_CLAUDE_RUNNER_MODEL } = require('../lib/runner-command');
22
23
  const { hasFlag } = require('../lib/arg-parser');
23
24
 
@@ -267,6 +268,14 @@ function tickCommand(args, root = process.cwd()) {
267
268
  orbIngestError,
268
269
  }));
269
270
 
271
+ // Dream-RSI: record this tick as one attempt when the workspace has the
272
+ // recorder. Best-effort; a missing or failing recorder never changes the
273
+ // tick's outcome. The engine spawns claude-backed runners by default.
274
+ const rsiLog = (m) => { if (!asJson) process.stderr.write(`rsi: ${m}\n`); };
275
+ const rsiAttempt = rsi.beginAttempt(root, { lane: rsi.IMPROVE_LANE, engine: 'claude', log: rsiLog });
276
+ const rsiBefore = rsiAttempt ? rsi.gitSnapshot(root) : null;
277
+ let rsiOutcome = null;
278
+
270
279
  let engine;
271
280
  let verify = { passed: null, cmd: verifyCmd };
272
281
  try {
@@ -312,6 +321,14 @@ function tickCommand(args, root = process.cwd()) {
312
321
  ? 'no due mission; heartbeat alive (no-op)'
313
322
  : `mission ${engine.reason}${changedTail}`;
314
323
 
324
+ rsiOutcome = {
325
+ status: !engine.ok || verify.passed === false ? 'failed' : producedWork ? 'shipped' : 'nothing',
326
+ verify: verify.passed === true ? 'pass' : verify.passed === false ? 'fail' : 'skipped',
327
+ elapsed_s: Math.round(elapsedMs / 100) / 10,
328
+ engine_calls: 1,
329
+ reason: String(what || '').slice(0, 200),
330
+ };
331
+
315
332
  const receipt = pulse.buildPulseReceipt({
316
333
  tickIndex,
317
334
  phase: 'finished',
@@ -426,11 +443,32 @@ function tickCommand(args, root = process.cwd()) {
426
443
  orb_policy_lesson: orbPolicy,
427
444
  };
428
445
  if (orbIngestError) out.orb_ingest_error = orbIngestError;
446
+ rsiOutcome = {
447
+ status: 'failed',
448
+ verify: 'skipped',
449
+ elapsed_s: Math.round((Date.now() - startedAt) / 100) / 10,
450
+ engine_calls: 1,
451
+ reason: String(err && err.message ? err.message : err).slice(-200),
452
+ };
429
453
  if (!asJson) process.stdout.write(`pulse tick #${tickIndex} crashed: ${out.error}\n`);
430
454
  return emit(out, asJson);
431
455
  } finally {
432
456
  process.removeListener('SIGINT', finishInterrupted);
433
457
  process.removeListener('SIGTERM', finishInterrupted);
458
+ if (rsiAttempt) {
459
+ const delta = rsi.gitDelta(root, rsiBefore);
460
+ rsi.finishAttempt(root, rsiAttempt, {
461
+ commits: delta.commits,
462
+ files: delta.files,
463
+ ...(rsiOutcome || {
464
+ status: 'failed',
465
+ verify: 'skipped',
466
+ elapsed_s: Math.round((Date.now() - startedAt) / 100) / 10,
467
+ engine_calls: 1,
468
+ reason: 'tick ended without an outcome',
469
+ }),
470
+ }, { log: rsiLog });
471
+ }
434
472
  pulse.releaseLock(root);
435
473
  }
436
474
  }
package/commands/push.js CHANGED
@@ -985,7 +985,7 @@ async function pushAtris() {
985
985
  console.log(` ... +${failedToLand.length - shown.length} more`);
986
986
  }
987
987
  console.log('');
988
- console.log(' Common causes: path is outside the workspace (e.g. absolute /Users/... path),');
988
+ console.log(' common causes: path is outside the workspace (for example, an absolute home path),');
989
989
  console.log(' your role lacks write permission for that folder, or warm runner returned an error.');
990
990
  console.log(' These files will appear as drift on your next push so you can retry.');
991
991
  }
@@ -9,22 +9,9 @@
9
9
  */
10
10
 
11
11
  const fs = require('fs');
12
- const os = require('os');
13
12
  const path = require('path');
14
13
  const { spawnSync } = require('child_process');
15
-
16
- function resolveBackendRoot() {
17
- const candidates = [
18
- process.env.ATRIS_BACKEND_DIR,
19
- path.resolve(__dirname, '..', '..', 'atrisos-backend'),
20
- path.resolve(process.cwd(), '..', 'atrisos-backend'),
21
- path.join(os.homedir(), 'arena', 'atrisos-backend'),
22
- ].filter(Boolean);
23
- for (const root of candidates) {
24
- if (fs.existsSync(path.join(root, 'scripts', 'rainmaker.py'))) return root;
25
- }
26
- return null;
27
- }
14
+ const { BACKEND_ROOT_HINT, resolveBackendRoot } = require('../utils/backend-root');
28
15
 
29
16
  function rainmakerCommand(args = []) {
30
17
  if (args[0] === '--help' || args[0] === '-h') {
@@ -32,8 +19,8 @@ function rainmakerCommand(args = []) {
32
19
  return 0;
33
20
  }
34
21
  const root = resolveBackendRoot();
35
- if (!root) {
36
- console.error('Cannot find scripts/rainmaker.py. Set ATRIS_BACKEND_DIR to atrisos-backend.');
22
+ if (!root || !fs.existsSync(path.join(root, 'scripts', 'rainmaker.py'))) {
23
+ console.error(BACKEND_ROOT_HINT);
37
24
  return 1;
38
25
  }
39
26
  const script = path.join(root, 'scripts', 'rainmaker.py');
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ // atris rsi - read the Dream-RSI attempt ledger in plain words.
4
+ //
5
+ // Prints trees and attempts by lane, the most recent day's attempts and
6
+ // outcomes, the live policy id, and the last dream receipt. Read-only: it
7
+ // never touches record.py, never writes state, and exits 0 even when the
8
+ // workspace has no ledger yet.
9
+
10
+ const fs = require('fs');
11
+ const rsi = require('../lib/rsi-record');
12
+
13
+ function readJsonl(file) {
14
+ try {
15
+ return fs.readFileSync(file, 'utf8')
16
+ .split(/\r?\n/)
17
+ .map((line) => line.trim())
18
+ .filter(Boolean)
19
+ .map((line) => { try { return JSON.parse(line); } catch { return null; } })
20
+ .filter(Boolean);
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ function dayOf(node) {
27
+ return String((node && node.created_at) || '').slice(0, 10);
28
+ }
29
+
30
+ function collectStatus(root) {
31
+ const nodes = rsi.latestNodes(root);
32
+ const lanes = new Map();
33
+ for (const n of nodes) {
34
+ const lane = String(n.lane || 'unknown');
35
+ if (!lanes.has(lane)) {
36
+ lanes.set(lane, { trees: new Set(), nodes: 0, shipped: 0, failed: 0, nothing: 0, open: 0 });
37
+ }
38
+ const l = lanes.get(lane);
39
+ l.trees.add(String(n.tree_id || ''));
40
+ l.nodes += 1;
41
+ const st = n.outcome && n.outcome.status;
42
+ if (st === 'shipped') l.shipped += 1;
43
+ else if (st === 'failed') l.failed += 1;
44
+ else if (st === 'nothing') l.nothing += 1;
45
+ else l.open += 1; // running / waiting / unlabeled
46
+ }
47
+ const days = [...new Set(nodes.map(dayOf).filter(Boolean))].sort();
48
+ const lastDay = days.length ? days[days.length - 1] : null;
49
+ const lastAttempts = lastDay ? nodes.filter((n) => dayOf(n) === lastDay) : [];
50
+ const policyFile = `${root}/atris/rsi/policy.current`;
51
+ const policy = fs.existsSync(policyFile) ? rsi.currentPolicyId(root) : 'p-0001 (default)';
52
+ const dreams = readJsonl(rsi.dreamsPath(root));
53
+ return {
54
+ lanes,
55
+ lastDay,
56
+ lastAttempts,
57
+ policy,
58
+ lastDream: dreams.length ? dreams[dreams.length - 1] : null,
59
+ totalNodes: nodes.length,
60
+ ledger: rsi.attemptsPath(root),
61
+ };
62
+ }
63
+
64
+ function outcomeWord(status, verify) {
65
+ if (status === 'shipped') return verify === 'pass' ? 'shipped, verify passed' : 'shipped';
66
+ if (status === 'failed') return 'failed';
67
+ if (status === 'nothing') return 'found nothing to do';
68
+ return 'still open';
69
+ }
70
+
71
+ function formatStatus(s) {
72
+ const lines = ['rsi attempts ledger', ` ${s.ledger}`, ''];
73
+ if (!s.totalNodes) {
74
+ lines.push(' no attempts recorded yet.');
75
+ lines.push('');
76
+ lines.push(` policy: ${s.policy}`);
77
+ if (s.lastDream) lines.push(` last dream: ${dreamLine(s.lastDream)}`);
78
+ else lines.push(' no dreams yet.');
79
+ return lines.join('\n');
80
+ }
81
+ const totalTrees = [...s.lanes.values()].reduce((a, l) => a + l.trees.size, 0);
82
+ lines.push(` ${totalTrees} tree${totalTrees === 1 ? '' : 's'}, ${s.totalNodes} attempt${s.totalNodes === 1 ? '' : 's'} across ${s.lanes.size} lane${s.lanes.size === 1 ? '' : 's'}:`);
83
+ for (const [lane, l] of [...s.lanes.entries()].sort()) {
84
+ const parts = [];
85
+ if (l.shipped) parts.push(`${l.shipped} shipped`);
86
+ if (l.failed) parts.push(`${l.failed} failed`);
87
+ if (l.nothing) parts.push(`${l.nothing} nothing`);
88
+ if (l.open) parts.push(`${l.open} open`);
89
+ lines.push(` ${lane}: ${l.trees.size} tree${l.trees.size === 1 ? '' : 's'}, ${l.nodes} attempt${l.nodes === 1 ? '' : 's'}${parts.length ? ` (${parts.join(', ')})` : ''}`);
90
+ }
91
+ lines.push('');
92
+ lines.push(` last night (${s.lastDay}): ${s.lastAttempts.length} attempt${s.lastAttempts.length === 1 ? '' : 's'}`);
93
+ for (const n of s.lastAttempts.slice(0, 10)) {
94
+ const reason = String((n.outcome && n.outcome.reason) || '').replace(/\s+/g, ' ').trim().slice(0, 80);
95
+ lines.push(` ${n.id}: ${outcomeWord(n.outcome && n.outcome.status, n.outcome && n.outcome.verify)}${reason ? ` - ${reason}` : ''}`);
96
+ }
97
+ if (s.lastAttempts.length > 10) lines.push(` ...and ${s.lastAttempts.length - 10} more`);
98
+ lines.push('');
99
+ lines.push(` policy: ${s.policy}`);
100
+ lines.push(s.lastDream ? ` last dream: ${dreamLine(s.lastDream)}` : ' no dreams yet.');
101
+ return lines.join('\n');
102
+ }
103
+
104
+ function dreamLine(receipt) {
105
+ const deployed = receipt.deployed ? `deployed ${receipt.deployed}` : `kept ${receipt.current || 'current policy'}`;
106
+ const reason = String(receipt.reason || '').replace(/\s+/g, ' ').trim().slice(0, 120);
107
+ return `${receipt.at || '(no date)'} · ${deployed}${reason ? ` · ${reason}` : ''}`;
108
+ }
109
+
110
+ function showHelp() {
111
+ console.log(`atris rsi - read the Dream-RSI attempt ledger
112
+
113
+ Usage:
114
+ atris rsi same as status
115
+ atris rsi status trees + attempts by lane, last night's outcomes, policy, last dream
116
+ atris rsi status --json machine-readable status
117
+
118
+ Read-only. State lives in .atris/state/rsi/ (or $ATRIS_RSI_STATE).`);
119
+ }
120
+
121
+ function run(argv = []) {
122
+ const args = Array.isArray(argv) ? argv : [];
123
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
124
+ showHelp();
125
+ return 0;
126
+ }
127
+ const sub = args[0] && !args[0].startsWith('-') ? args[0] : 'status';
128
+ if (sub !== 'status') {
129
+ console.log(`unknown rsi subcommand: ${sub}`);
130
+ showHelp();
131
+ return 2;
132
+ }
133
+ const root = process.cwd();
134
+ const s = collectStatus(root);
135
+ if (args.includes('--json')) {
136
+ const lanes = {};
137
+ for (const [lane, l] of s.lanes.entries()) {
138
+ lanes[lane] = { trees: l.trees.size, nodes: l.nodes, shipped: l.shipped, failed: l.failed, nothing: l.nothing, open: l.open };
139
+ }
140
+ console.log(JSON.stringify({
141
+ ok: true,
142
+ ledger: s.ledger,
143
+ total_nodes: s.totalNodes,
144
+ lanes,
145
+ last_day: s.lastDay,
146
+ last_day_attempts: s.lastAttempts.length,
147
+ policy: s.policy,
148
+ last_dream: s.lastDream,
149
+ }));
150
+ return 0;
151
+ }
152
+ console.log(formatStatus(s));
153
+ return 0;
154
+ }
155
+
156
+ module.exports = { run, collectStatus, formatStatus };
@@ -4,7 +4,7 @@
4
4
  * atris spaceship, bounded, self-reporting overnight runner.
5
5
  *
6
6
  * Thin wrapper over scripts/spaceship.sh. The script is the implementation
7
- * (a supervised loop that survives bad ticks and emails Keshav on every
7
+ * (a supervised loop that survives bad ticks and emails the operator on every
8
8
  * meaningful state change); this module just makes it reachable as
9
9
  * `atris spaceship ...` and integrates with the CLI's command dispatch.
10
10
  *