atris 3.58.5 → 3.58.7
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/README.md +8 -0
- 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/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +3 -3
- package/atris/skills/x-search/SKILL.md +2 -2
- package/atris/skills/youtube/SKILL.md +44 -28
- package/bin/atris.js +56 -3
- package/commands/auth.js +58 -24
- package/commands/brain.js +1 -0
- package/commands/design.js +362 -0
- package/commands/doc-health.js +329 -0
- package/commands/drive.js +32 -0
- package/commands/improve.js +67 -1
- package/commands/land.js +144 -4
- package/commands/learn.js +211 -40
- package/commands/member.js +65 -11
- package/commands/mission.js +37 -7
- package/commands/pulse.js +38 -0
- package/commands/rsi.js +156 -0
- package/commands/task.js +41 -1
- package/commands/workflow.js +15 -14
- package/commands/x-search.js +9 -10
- package/commands/youtube.js +518 -107
- package/lib/apply-gate.js +22 -4
- 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/known-commands.js +3 -3
- package/lib/member-context.js +42 -0
- package/lib/rsi-record.js +335 -0
- package/lib/state-detection.js +8 -8
- package/lib/task-db.js +71 -51
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +9 -3
- package/lib/todo.js +22 -10
- package/mcp/atris-mcp/index.mjs +174 -0
- package/package.json +8 -3
- package/scripts/det/ytnotes +122 -10
- package/utils/auth.js +109 -13
package/commands/mission.js
CHANGED
|
@@ -6,6 +6,7 @@ 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,
|
|
@@ -111,6 +112,15 @@ const CODEX_NATIVE_GOAL_CLOSED_STATUSES = new Set(['complete', 'completed', 'ach
|
|
|
111
112
|
const DEFAULT_LONG_RUN_VERIFIER = 'git diff --check';
|
|
112
113
|
const SLEEP_LENGTH_BUDGET_SECONDS = 3600;
|
|
113
114
|
const HUMAN_BLOCKING_PAUSE_REASONS = new Set(['auth-required', 'model-unavailable', 'rate-limit-exceeded-wall']);
|
|
115
|
+
// Claude prints its logged-out message on stdout ("Invalid API key · Please
|
|
116
|
+
// run /login"), not stderr, so the check has to read the captured result text
|
|
117
|
+
// too or a dead login masquerades as a generic claude-error.
|
|
118
|
+
const MISSION_RUNNER_AUTH_EXPIRED_RE = /not authenticated|please log in|login required|auth(?:entication)? expired|invalid (?:api|connection) key|run \/login/i;
|
|
119
|
+
|
|
120
|
+
function missionRunnerAuthExpired(...texts) {
|
|
121
|
+
return texts.some((text) => Boolean(text) && MISSION_RUNNER_AUTH_EXPIRED_RE.test(String(text)));
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
const MISSION_BUDGET_TIERS = Object.freeze({
|
|
115
125
|
quick: Object.freeze({ max_ticks: 4, requested_seconds: 15 * 60 }),
|
|
116
126
|
long: Object.freeze({ max_ticks: 12, requested_seconds: 60 * 60 }),
|
|
@@ -8406,8 +8416,12 @@ function probeClaudeBinary() {
|
|
|
8406
8416
|
// Pull unread operator pings off the mission and mark them consumed, so the
|
|
8407
8417
|
// next tick's prompt carries them exactly once. Pings are how a human talks to
|
|
8408
8418
|
// an always-on member mid-run: atris member ping <name> "<msg>".
|
|
8419
|
+
function pendingMissionPings(mission) {
|
|
8420
|
+
return (Array.isArray(mission.pings) ? mission.pings : []).filter((p) => p && !p.consumed_at);
|
|
8421
|
+
}
|
|
8422
|
+
|
|
8409
8423
|
function consumeMissionPings(mission, cwd) {
|
|
8410
|
-
const pending = (
|
|
8424
|
+
const pending = pendingMissionPings(mission);
|
|
8411
8425
|
if (!pending.length) return { mission, pings: [] };
|
|
8412
8426
|
const consumedAt = stampIso();
|
|
8413
8427
|
const pings = (mission.pings || []).map((p) => (p && !p.consumed_at ? { ...p, consumed_at: consumedAt } : p));
|
|
@@ -8415,7 +8429,8 @@ function consumeMissionPings(mission, cwd) {
|
|
|
8415
8429
|
return { mission: saved, pings: pending };
|
|
8416
8430
|
}
|
|
8417
8431
|
|
|
8418
|
-
function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
|
|
8432
|
+
function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd = process.cwd()) {
|
|
8433
|
+
const sharedProcess = memberProcessPrompt(cwd);
|
|
8419
8434
|
const pingLines = pings.length
|
|
8420
8435
|
? [
|
|
8421
8436
|
``,
|
|
@@ -8447,6 +8462,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
|
|
|
8447
8462
|
`**Last tick:** ${mission.last_tick_at || 'never'}`,
|
|
8448
8463
|
...missionBudgetPromptLines(mission),
|
|
8449
8464
|
...checkFeedbackLines,
|
|
8465
|
+
...(sharedProcess ? ['', sharedProcess] : []),
|
|
8450
8466
|
``,
|
|
8451
8467
|
`## Your task`,
|
|
8452
8468
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
@@ -8765,7 +8781,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8765
8781
|
const finalText = String(stdout || '').trim();
|
|
8766
8782
|
const errStr = String(stderr || '').slice(-2000);
|
|
8767
8783
|
const ok = code === 0 && !timedOut && !aborted;
|
|
8768
|
-
const authExpired =
|
|
8784
|
+
const authExpired = !ok && missionRunnerAuthExpired(errStr, finalText);
|
|
8769
8785
|
resolve({
|
|
8770
8786
|
ok,
|
|
8771
8787
|
brief_id: briefId,
|
|
@@ -8940,7 +8956,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8940
8956
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8941
8957
|
const ok = code === 0 && !isError && !timedOut && !aborted;
|
|
8942
8958
|
const errStr = stderr.slice(-2000);
|
|
8943
|
-
const authExpired =
|
|
8959
|
+
const authExpired = !ok && missionRunnerAuthExpired(errStr, finalText, stdoutBuf);
|
|
8944
8960
|
resolve({
|
|
8945
8961
|
ok,
|
|
8946
8962
|
timedOut,
|
|
@@ -9521,6 +9537,9 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9521
9537
|
drill: drillResult,
|
|
9522
9538
|
};
|
|
9523
9539
|
} else if (tickAtris2Runner) {
|
|
9540
|
+
// Build before consuming direction or allocating the guard: a shared
|
|
9541
|
+
// process read error must leave operator pings available for retry.
|
|
9542
|
+
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pendingMissionPings(mission), cwd);
|
|
9524
9543
|
let atris2GitGuard = null;
|
|
9525
9544
|
try {
|
|
9526
9545
|
atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
@@ -9541,7 +9560,6 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9541
9560
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
9542
9561
|
mission = pingDrain.mission;
|
|
9543
9562
|
runtimeMission = runtimeView(mission);
|
|
9544
|
-
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
9545
9563
|
const { runAtris2Turn } = require('./probe');
|
|
9546
9564
|
const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
|
|
9547
9565
|
const tickController = new AbortController();
|
|
@@ -9593,12 +9611,12 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9593
9611
|
}
|
|
9594
9612
|
}
|
|
9595
9613
|
} else {
|
|
9614
|
+
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pendingMissionPings(mission), cwd);
|
|
9596
9615
|
let sessionMode = sessionId ? 'resume' : 'set';
|
|
9597
9616
|
let useId = sessionId || pendingSessionId;
|
|
9598
9617
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
9599
9618
|
mission = pingDrain.mission;
|
|
9600
9619
|
runtimeMission = runtimeView(mission);
|
|
9601
|
-
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
9602
9620
|
const restoreTickRunnerProfile = tickEngineId ? applyMissionRunnerProfile(tickEngineId) : () => {};
|
|
9603
9621
|
let claudeResult;
|
|
9604
9622
|
let sessionBusyRetried = false;
|
|
@@ -9813,6 +9831,14 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9813
9831
|
const claudeRanTick = Boolean(result.claude) && result.status === 'ran';
|
|
9814
9832
|
const claudeSessionTicks = claudeRanTick ? Number(mission.claude_session_ticks || 0) + 1 : Number(mission.claude_session_ticks || 0);
|
|
9815
9833
|
const rotateSessionForContext = claudeRanTick && claudeSessionTicks >= CLAUDE_SESSION_CONTEXT_ROTATE_TICKS;
|
|
9834
|
+
// Heartbeat missions run one tick per invocation, so the in-run ticks
|
|
9835
|
+
// array alone can never see two identical errors. Persist the trailing
|
|
9836
|
+
// same-reason errored count so the breaker below trips across runs.
|
|
9837
|
+
const errorStreakCount = result.status === 'errored' && result.reason
|
|
9838
|
+
? (latestOnDisk.last_tick_status === 'errored' && latestOnDisk.last_tick_reason === result.reason
|
|
9839
|
+
? Number(latestOnDisk.error_streak_count || 0) + 1
|
|
9840
|
+
: 1)
|
|
9841
|
+
: 0;
|
|
9816
9842
|
// Base on latestOnDisk so mid-tick complete proof/completed_at survive.
|
|
9817
9843
|
mission = saveMission({
|
|
9818
9844
|
...latestOnDisk,
|
|
@@ -9825,6 +9851,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9825
9851
|
last_tick_index: tickIdx,
|
|
9826
9852
|
last_tick_layer: result.layer,
|
|
9827
9853
|
last_tick_layer_source: result.layer_source,
|
|
9854
|
+
error_streak_count: errorStreakCount,
|
|
9828
9855
|
verifier_result: verifierResult || (verifyEach && result.protected_lane_guard && result.protected_lane_guard.allowed === false ? null : latestOnDisk.verifier_result) || null,
|
|
9829
9856
|
last_check_feedback: verifierResult
|
|
9830
9857
|
? extractCheckFeedback(verifierResult)
|
|
@@ -9902,9 +9929,12 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9902
9929
|
// is the same trap one step less deterministic: keep retrying and the loop burns every
|
|
9903
9930
|
// tick + cron firing on it. Halt at two-in-a-row and surface the reason for a human.
|
|
9904
9931
|
const errStreak = consecutiveSameReasonErrors(ticks);
|
|
9932
|
+
// The in-run ticks array only sees this invocation; the persisted streak
|
|
9933
|
+
// carries identical errors across heartbeat runs of one tick each.
|
|
9934
|
+
const errStreakCount = Math.max(errStreak.count, errStreak.reason ? Number(mission.error_streak_count || 0) : 0);
|
|
9905
9935
|
// Sleeping Atris2 backends are different: leave the mission running so the
|
|
9906
9936
|
// next tick or heartbeat can catch the backend after it wakes.
|
|
9907
|
-
if (
|
|
9937
|
+
if (errStreakCount >= 2 && !missionRunKeepsRetryingError(errStreak.reason)) { pauseReason = `repeated-error:${errStreak.reason}`; break; }
|
|
9908
9938
|
|
|
9909
9939
|
// Sleep until next tick
|
|
9910
9940
|
let sleepMs = 0;
|
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/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/task.js
CHANGED
|
@@ -253,6 +253,7 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
253
253
|
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
254
254
|
--from-failed opts in to relabel a fail-closed row (never done)
|
|
255
255
|
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
256
|
+
atris task keep [--json] Put away finished and untouched work, then refresh the list
|
|
256
257
|
atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
|
|
257
258
|
atris task relabel-archived [--dry-run|--apply]
|
|
258
259
|
One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
|
|
@@ -6416,7 +6417,7 @@ function compactTechnicalDetails(task, formatTitle = value => value) {
|
|
|
6416
6417
|
}
|
|
6417
6418
|
|
|
6418
6419
|
function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
6419
|
-
const active = tasks.filter(task => task.status !== 'done');
|
|
6420
|
+
const active = tasks.filter(task => task.status !== 'done' && task.status !== 'archived');
|
|
6420
6421
|
const staleFailed = [];
|
|
6421
6422
|
const visible = [];
|
|
6422
6423
|
for (const task of active) {
|
|
@@ -6446,7 +6447,43 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
|
6446
6447
|
return { groups: grouped, staleFailed };
|
|
6447
6448
|
}
|
|
6448
6449
|
|
|
6450
|
+
function refreshKeptTaskList(cwd = process.cwd()) {
|
|
6451
|
+
const taskDb = getTaskDb();
|
|
6452
|
+
const db = taskDb.open();
|
|
6453
|
+
writeDefaultProjection(taskDb, db);
|
|
6454
|
+
autoRenderTodoFromDb(cwd);
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6457
|
+
function keptCount(result) {
|
|
6458
|
+
return (result && result.put_away ? result.put_away.length : 0)
|
|
6459
|
+
+ (result && result.reaped ? result.reaped.length : 0);
|
|
6460
|
+
}
|
|
6461
|
+
|
|
6462
|
+
function cmdKeep(args) {
|
|
6463
|
+
const result = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
|
|
6464
|
+
if (keptCount(result)) refreshKeptTaskList(process.cwd());
|
|
6465
|
+
const count = keptCount(result);
|
|
6466
|
+
if (wantsJson(args)) {
|
|
6467
|
+
printJson({
|
|
6468
|
+
ok: true,
|
|
6469
|
+
action: 'keep',
|
|
6470
|
+
count,
|
|
6471
|
+
put_away: result.put_away,
|
|
6472
|
+
reaped: result.reaped,
|
|
6473
|
+
});
|
|
6474
|
+
return;
|
|
6475
|
+
}
|
|
6476
|
+
if (!count) {
|
|
6477
|
+
console.log('task list is current. nothing to put away.');
|
|
6478
|
+
return;
|
|
6479
|
+
}
|
|
6480
|
+
const noun = count === 1 ? 'item' : 'items';
|
|
6481
|
+
console.log(`put away ${count} ${noun} that were finished or sitting still.`);
|
|
6482
|
+
}
|
|
6483
|
+
|
|
6449
6484
|
function cmdDay(args) {
|
|
6485
|
+
const kept = require('../lib/task-list-keeper').keepWorkspaceTaskList(process.cwd());
|
|
6486
|
+
if (keptCount(kept)) autoRenderTodoFromDb(process.cwd());
|
|
6450
6487
|
const all = hasFlag(args, '--all');
|
|
6451
6488
|
const full = hasFlag(args, '--full');
|
|
6452
6489
|
const everywhere = taskScopeEverywhere(args);
|
|
@@ -13321,6 +13358,7 @@ async function runTaskCommand(args) {
|
|
|
13321
13358
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
13322
13359
|
case 'archive': return cmdArchive(rest);
|
|
13323
13360
|
case 'clear-done': return cmdClearDone(rest);
|
|
13361
|
+
case 'keep': return cmdKeep(rest);
|
|
13324
13362
|
case 'reap-mission-blockers':
|
|
13325
13363
|
case 'reap-blockers':
|
|
13326
13364
|
return cmdReapMissionBlockers(rest);
|
|
@@ -13425,6 +13463,8 @@ module.exports = {
|
|
|
13425
13463
|
delegateTask,
|
|
13426
13464
|
AGENT_ENV_MARKERS,
|
|
13427
13465
|
autoRenderTodoFromDb,
|
|
13466
|
+
refreshKeptTaskList,
|
|
13467
|
+
keptCount,
|
|
13428
13468
|
projectionMissions,
|
|
13429
13469
|
projectionWishes,
|
|
13430
13470
|
taskBoardViewModel,
|
package/commands/workflow.js
CHANGED
|
@@ -19,6 +19,16 @@ const { startFirstTalk } = require('../lib/context-gatherer');
|
|
|
19
19
|
const { isNonInteractive } = require('../lib/noninteractive');
|
|
20
20
|
const { loadContext } = require('../lib/state-detection');
|
|
21
21
|
const { buildToolResultBody } = require('../lib/tool-result-encode');
|
|
22
|
+
const { commitReviewLearning } = require('./learn');
|
|
23
|
+
|
|
24
|
+
function appendReviewLearningToJournal(journalContent, learning) {
|
|
25
|
+
const text = String(journalContent || '').replace(/\r\n/g, '\n');
|
|
26
|
+
const note = String(learning || '');
|
|
27
|
+
if (text.includes('## Notes')) {
|
|
28
|
+
return text.replace(/## Notes\n/, `## Notes\n${note}\n`);
|
|
29
|
+
}
|
|
30
|
+
return `${text}\n## Notes\n${note}\n`;
|
|
31
|
+
}
|
|
22
32
|
|
|
23
33
|
function wrapWorkflowText(text, width = 76) {
|
|
24
34
|
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
@@ -1774,27 +1784,17 @@ async function reviewAtris() {
|
|
|
1774
1784
|
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
1775
1785
|
const learning = `- ${timestamp} \u2014 ${answer.trim()}`;
|
|
1776
1786
|
|
|
1777
|
-
|
|
1778
|
-
if (journalContent.includes('## Notes')) {
|
|
1779
|
-
journalContent = journalContent.replace(/## Notes\n/, `## Notes\n${learning}\n`);
|
|
1780
|
-
} else {
|
|
1781
|
-
journalContent += `\n## Notes\n${learning}\n`;
|
|
1782
|
-
}
|
|
1787
|
+
journalContent = appendReviewLearningToJournal(journalContent, learning);
|
|
1783
1788
|
|
|
1784
1789
|
fs.writeFileSync(logFile, journalContent);
|
|
1785
1790
|
console.log('');
|
|
1786
1791
|
console.log(`✓ Logged to journal: ${learning}`);
|
|
1787
1792
|
}
|
|
1788
1793
|
|
|
1789
|
-
// Also log to structured learnings (if learnings module exists)
|
|
1790
1794
|
try {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
const type = /^(don't|never|avoid|watch out|careful)/i.test(insight) ? 'pitfall' : 'pattern';
|
|
1795
|
-
const key = insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
|
|
1796
|
-
addLearning({ type, key, insight, confidence: 7, source: 'review', files: [] });
|
|
1797
|
-
console.log(`✓ Saved to learnings: [7/10] ${type}/${key}`);
|
|
1795
|
+
commitReviewLearning(answer.trim(), {
|
|
1796
|
+
cwd: process.cwd(),
|
|
1797
|
+
});
|
|
1798
1798
|
} catch {
|
|
1799
1799
|
// learnings module not available, so skip silently
|
|
1800
1800
|
}
|
|
@@ -1897,6 +1897,7 @@ module.exports = {
|
|
|
1897
1897
|
planAtris,
|
|
1898
1898
|
doAtris,
|
|
1899
1899
|
reviewAtris,
|
|
1900
|
+
appendReviewLearningToJournal,
|
|
1900
1901
|
renderReviewMinute,
|
|
1901
1902
|
executorAgentPrompt,
|
|
1902
1903
|
executorDispatchForTask,
|
package/commands/x-search.js
CHANGED
|
@@ -32,7 +32,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
|
32
32
|
output('Prints to stdout. Rich ephemeral prints one apply next-step, then hands off to atris youtube search (no files).');
|
|
33
33
|
output('--save files a brief only when the result is rich.');
|
|
34
34
|
output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
|
|
35
|
-
output('Empty or failed search
|
|
35
|
+
output('Empty or failed search prints credits refunded only when the server marks a refund.');
|
|
36
36
|
output('');
|
|
37
37
|
output('Options:');
|
|
38
38
|
output(' --limit <n> Max results hint (search only)');
|
|
@@ -327,13 +327,6 @@ function xSearchCredits(data) {
|
|
|
327
327
|
return { used, remaining, refunded };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
function creditsWereRefunded(credits) {
|
|
331
|
-
if (!credits) return false;
|
|
332
|
-
if (credits.used === 0) return true;
|
|
333
|
-
if (credits.refunded === true) return true;
|
|
334
|
-
return typeof credits.refunded === 'number' && credits.refunded > 0;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
330
|
function creditsRefundedExplicitly(credits) {
|
|
338
331
|
if (!credits) return false;
|
|
339
332
|
if (credits.refunded === true) return true;
|
|
@@ -345,7 +338,7 @@ function formatCreditsLines(credits) {
|
|
|
345
338
|
if (credits.used !== undefined || credits.remaining !== undefined) {
|
|
346
339
|
lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
|
|
347
340
|
}
|
|
348
|
-
if (
|
|
341
|
+
if (creditsRefundedExplicitly(credits)) {
|
|
349
342
|
lines.push('credits refunded');
|
|
350
343
|
}
|
|
351
344
|
return lines;
|
|
@@ -360,7 +353,7 @@ function xSearchFailureError(result) {
|
|
|
360
353
|
? ' xAI is unavailable; retry in a few seconds.'
|
|
361
354
|
: '';
|
|
362
355
|
const credits = xSearchCredits(result.data);
|
|
363
|
-
const refundHint = result.status === 502 &&
|
|
356
|
+
const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
|
|
364
357
|
? ' credits refunded.'
|
|
365
358
|
: '';
|
|
366
359
|
const lines = [`X search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
@@ -398,6 +391,12 @@ async function runXSearch(options, deps = {}) {
|
|
|
398
391
|
forceMint: true,
|
|
399
392
|
});
|
|
400
393
|
if (remint?.ok && remint.token) {
|
|
394
|
+
if (!options.json) {
|
|
395
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
396
|
+
for (const line of formatCreditsLines(xSearchCredits(result.data))) {
|
|
397
|
+
print(line);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
401
400
|
auth = remint;
|
|
402
401
|
result = await call(auth.token);
|
|
403
402
|
}
|