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.
- package/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +9 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/policies/engineering-principles.md +129 -0
- package/atris/policies/genesis.md +112 -0
- package/atris/policies/product-design-principles.md +100 -0
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +42 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/atris/skills/youtube/SKILL.md +11 -11
- package/bin/atris.js +60 -8
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/auth.js +8 -2
- package/commands/autopilot.js +4 -3
- package/commands/avail.js +1 -1
- package/commands/brain.js +4 -1
- package/commands/business.js +13 -8
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/design.js +362 -0
- package/commands/doc-health.js +377 -0
- package/commands/drive.js +32 -0
- package/commands/experiments.js +13 -4
- package/commands/improve.js +68 -2
- package/commands/integrations.js +2 -5
- package/commands/land.js +144 -4
- package/commands/learn.js +7 -2
- package/commands/member.js +239 -58
- package/commands/mission.js +94 -18
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/pulse.js +38 -0
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/rsi.js +156 -0
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +54 -3
- package/commands/workflow.js +17 -7
- package/commands/youtube.js +76 -8
- package/lib/apply-gate.js +22 -4
- package/lib/codex-flight.js +28 -5
- package/lib/daily-log.js +88 -0
- package/lib/design-api.js +130 -0
- package/lib/engine-ask.js +1 -1
- package/lib/first-minute.js +1 -6
- package/lib/fleet.js +4 -1
- package/lib/improve-vitals-html.js +3 -2
- package/lib/known-commands.js +3 -3
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +183 -0
- package/lib/member-context.js +42 -0
- package/lib/mission-room.js +4 -4
- package/lib/noninteractive.js +15 -4
- package/lib/policy-lessons.js +1 -1
- package/lib/receipt-block.js +137 -28
- package/lib/rsi-record.js +335 -0
- package/lib/runner-command.js +1 -1
- package/lib/scorecard.js +4 -4
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +36 -17
- package/lib/task-db.js +74 -53
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +10 -4
- package/lib/todo.js +22 -10
- package/lib/wish-delegate.js +19 -5
- package/mcp/atris-mcp/index.mjs +227 -0
- package/package.json +4 -2
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/scripts/det/ytnotes +43 -8
- package/utils/api.js +1 -1
- package/utils/auth.js +79 -14
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/commands/mission.js
CHANGED
|
@@ -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', '
|
|
3562
|
-
['proof_gate', '
|
|
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 '
|
|
3662
|
-
if (memoryIds.has('plain_language') && candidateIds.has('usability')) return '
|
|
3663
|
-
if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return '
|
|
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 = (
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
|
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') ? '
|
|
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 =
|
|
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 ? '
|
|
461
|
-
+ (
|
|
466
|
+
+ ` · ${ok ? 'pass' : 'fail'} · ${secs}s · tools=${toolsRun}`
|
|
467
|
+
+ (calendar ? ` cal_cli=${cliCalendarOps}` : '')
|
|
462
468
|
+ ` · ${detail}`;
|
|
463
|
-
|
|
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(/^\//, '')}
|
|
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('
|
|
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
|
}
|
package/commands/rainmaker.js
CHANGED
|
@@ -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(
|
|
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');
|
package/commands/rsi.js
ADDED
|
@@ -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 };
|
package/commands/spaceship.js
CHANGED
|
@@ -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
|
|
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
|
*
|