atris 3.58.6 → 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/youtube/SKILL.md +11 -11
- package/bin/atris.js +56 -3
- package/commands/auth.js +8 -2
- 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 +6 -1
- 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 +11 -6
- package/commands/youtube.js +76 -8
- 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 +43 -8
- package/utils/auth.js +62 -7
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
|
@@ -21,6 +21,15 @@ const { loadContext } = require('../lib/state-detection');
|
|
|
21
21
|
const { buildToolResultBody } = require('../lib/tool-result-encode');
|
|
22
22
|
const { commitReviewLearning } = require('./learn');
|
|
23
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
|
+
}
|
|
32
|
+
|
|
24
33
|
function wrapWorkflowText(text, width = 76) {
|
|
25
34
|
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
26
35
|
if (!normalized) return [''];
|
|
@@ -1775,12 +1784,7 @@ async function reviewAtris() {
|
|
|
1775
1784
|
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
1776
1785
|
const learning = `- ${timestamp} \u2014 ${answer.trim()}`;
|
|
1777
1786
|
|
|
1778
|
-
|
|
1779
|
-
if (journalContent.includes('## Notes')) {
|
|
1780
|
-
journalContent = journalContent.replace(/## Notes\n/, `## Notes\n${learning}\n`);
|
|
1781
|
-
} else {
|
|
1782
|
-
journalContent += `\n## Notes\n${learning}\n`;
|
|
1783
|
-
}
|
|
1787
|
+
journalContent = appendReviewLearningToJournal(journalContent, learning);
|
|
1784
1788
|
|
|
1785
1789
|
fs.writeFileSync(logFile, journalContent);
|
|
1786
1790
|
console.log('');
|
|
@@ -1893,6 +1897,7 @@ module.exports = {
|
|
|
1893
1897
|
planAtris,
|
|
1894
1898
|
doAtris,
|
|
1895
1899
|
reviewAtris,
|
|
1900
|
+
appendReviewLearningToJournal,
|
|
1896
1901
|
renderReviewMinute,
|
|
1897
1902
|
executorAgentPrompt,
|
|
1898
1903
|
executorDispatchForTask,
|
package/commands/youtube.js
CHANGED
|
@@ -283,6 +283,51 @@ function parseVttTimestampMs(value) {
|
|
|
283
283
|
return ((hours * 3600) + (minutes * 60) + seconds) * 1000 + millis;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
function parseCleanTimestampMs(value) {
|
|
287
|
+
const match = String(value || '').trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
|
|
288
|
+
if (!match) return null;
|
|
289
|
+
const hours = Number(match[1] || 0);
|
|
290
|
+
const minutes = Number(match[2] || 0);
|
|
291
|
+
const seconds = Number(match[3] || 0);
|
|
292
|
+
if (![hours, minutes, seconds].every(Number.isFinite)) return null;
|
|
293
|
+
if (minutes > 59 || seconds > 59) return null;
|
|
294
|
+
return ((hours * 3600) + (minutes * 60) + seconds) * 1000;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseCleanTranscriptCues(raw) {
|
|
298
|
+
const cues = [];
|
|
299
|
+
let startMs = 0;
|
|
300
|
+
let sawStamp = false;
|
|
301
|
+
const pending = [];
|
|
302
|
+
const flush = () => {
|
|
303
|
+
const text = pending.join(' ').replace(/\s+/g, ' ').trim();
|
|
304
|
+
pending.length = 0;
|
|
305
|
+
if (!text) return;
|
|
306
|
+
const cue = { startMs, text };
|
|
307
|
+
if (cues.length && cues[cues.length - 1].text === cue.text && cues[cues.length - 1].startMs === cue.startMs) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
cues.push(cue);
|
|
311
|
+
};
|
|
312
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
313
|
+
const stripped = line.trim();
|
|
314
|
+
if (!stripped) continue;
|
|
315
|
+
const stamp = stripped.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]$/);
|
|
316
|
+
if (stamp) {
|
|
317
|
+
flush();
|
|
318
|
+
const parsed = parseCleanTimestampMs(stamp[1]);
|
|
319
|
+
if (parsed != null) {
|
|
320
|
+
startMs = parsed;
|
|
321
|
+
sawStamp = true;
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
pending.push(stripped.replace(/<[^>]+>/g, ''));
|
|
326
|
+
}
|
|
327
|
+
flush();
|
|
328
|
+
return sawStamp && cues.length ? cues : [];
|
|
329
|
+
}
|
|
330
|
+
|
|
286
331
|
function fetchCaptionText(urlString, redirects = 0) {
|
|
287
332
|
if (!captionHostAllowed(urlString)) {
|
|
288
333
|
return Promise.resolve(null);
|
|
@@ -380,7 +425,7 @@ function parseCaptionCues(raw) {
|
|
|
380
425
|
return cues;
|
|
381
426
|
}
|
|
382
427
|
|
|
383
|
-
return
|
|
428
|
+
return parseCleanTranscriptCues(raw);
|
|
384
429
|
}
|
|
385
430
|
|
|
386
431
|
function parseCaptionText(raw) {
|
|
@@ -415,7 +460,14 @@ function parseCaptionText(raw) {
|
|
|
415
460
|
}
|
|
416
461
|
|
|
417
462
|
function parseYtDlpInfoJson(result) {
|
|
418
|
-
const
|
|
463
|
+
const kept = [];
|
|
464
|
+
for (const line of String((result && result.stdout) || '').split(/\r?\n/)) {
|
|
465
|
+
const trimmed = line.trim();
|
|
466
|
+
if (!trimmed) continue;
|
|
467
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
468
|
+
kept.push(trimmed);
|
|
469
|
+
}
|
|
470
|
+
const raw = kept.join('\n').trim();
|
|
419
471
|
if (!raw) return null;
|
|
420
472
|
try {
|
|
421
473
|
const info = JSON.parse(raw);
|
|
@@ -426,7 +478,7 @@ function parseYtDlpInfoJson(result) {
|
|
|
426
478
|
}
|
|
427
479
|
|
|
428
480
|
function localCaptionNames(id) {
|
|
429
|
-
// scripts/det/ytnotes keeps
|
|
481
|
+
// scripts/det/ytnotes keeps these VTT names plus leftover yt_<id>.clean.txt.
|
|
430
482
|
return [
|
|
431
483
|
`yt_${id}.en.vtt`,
|
|
432
484
|
`yt_${id}.en-orig.vtt`,
|
|
@@ -765,6 +817,10 @@ function applySidecarRel(id) {
|
|
|
765
817
|
return applyGate.applySidecarRel('youtube', id);
|
|
766
818
|
}
|
|
767
819
|
|
|
820
|
+
function notesApplyRel(id) {
|
|
821
|
+
return applyGate.applySidecarRel('notes', experimentIdToken(id));
|
|
822
|
+
}
|
|
823
|
+
|
|
768
824
|
function notesExperimentSlug(id) {
|
|
769
825
|
return `notes-${experimentIdToken(id)}`;
|
|
770
826
|
}
|
|
@@ -814,7 +870,7 @@ function saveRichNotes(url, deps = {}) {
|
|
|
814
870
|
url,
|
|
815
871
|
lesson,
|
|
816
872
|
slug: id ? notesExperimentSlug(id) : null,
|
|
817
|
-
applyRel: id ?
|
|
873
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
818
874
|
});
|
|
819
875
|
return { thin: false, brief, packRel, lesson };
|
|
820
876
|
}
|
|
@@ -826,7 +882,7 @@ function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
|
|
|
826
882
|
return applyGate.ensureApply({
|
|
827
883
|
cwd,
|
|
828
884
|
source: url,
|
|
829
|
-
rel: id ?
|
|
885
|
+
rel: id ? notesApplyRel(id) : null,
|
|
830
886
|
now,
|
|
831
887
|
output,
|
|
832
888
|
incompleteMessage: slug
|
|
@@ -907,6 +963,7 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
907
963
|
};
|
|
908
964
|
add(briefRel);
|
|
909
965
|
add(applyRel);
|
|
966
|
+
add(notesApplyRel(id));
|
|
910
967
|
for (const section of sections) {
|
|
911
968
|
add(teachBriefRel(id, section));
|
|
912
969
|
add(applySidecarRel(`${id}-s${section}`));
|
|
@@ -959,6 +1016,7 @@ function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
|
959
1016
|
output,
|
|
960
1017
|
incompleteMessage: PROCESS_APPLY_MESSAGE,
|
|
961
1018
|
required: true,
|
|
1019
|
+
human: true,
|
|
962
1020
|
});
|
|
963
1021
|
}
|
|
964
1022
|
|
|
@@ -1455,15 +1513,22 @@ function channelVideosUrl(channel) {
|
|
|
1455
1513
|
return `${base}/videos`;
|
|
1456
1514
|
}
|
|
1457
1515
|
|
|
1516
|
+
function looksLikeFlatVideoId(id) {
|
|
1517
|
+
const text = String(id || '');
|
|
1518
|
+
if (/^(NA|None)$/i.test(text)) return false;
|
|
1519
|
+
return /^[A-Za-z0-9_-]+$/.test(text);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1458
1522
|
function parseFlatPlaylist(stdout) {
|
|
1459
1523
|
const videos = [];
|
|
1460
1524
|
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
1461
1525
|
const trimmed = line.trim();
|
|
1462
1526
|
if (!trimmed || !trimmed.includes('|')) continue;
|
|
1527
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
1463
1528
|
const idx = trimmed.indexOf('|');
|
|
1464
1529
|
const id = trimmed.slice(0, idx).trim();
|
|
1465
1530
|
const title = trimmed.slice(idx + 1).trim();
|
|
1466
|
-
if (id
|
|
1531
|
+
if (looksLikeFlatVideoId(id)) videos.push({ id, title });
|
|
1467
1532
|
}
|
|
1468
1533
|
return videos;
|
|
1469
1534
|
}
|
|
@@ -1744,6 +1809,7 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
|
1744
1809
|
const result = spawn('yt-dlp', [
|
|
1745
1810
|
'--no-update',
|
|
1746
1811
|
'--flat-playlist',
|
|
1812
|
+
'--no-warnings',
|
|
1747
1813
|
'--print',
|
|
1748
1814
|
'%(id)s|%(title)s',
|
|
1749
1815
|
playlistUrl,
|
|
@@ -1938,7 +2004,7 @@ function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
|
|
|
1938
2004
|
}));
|
|
1939
2005
|
const baseline = proveSavedLearnerBaseline({
|
|
1940
2006
|
cwd: deps.cwd || process.cwd(),
|
|
1941
|
-
applyRel: id ?
|
|
2007
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
1942
2008
|
lesson,
|
|
1943
2009
|
output,
|
|
1944
2010
|
json: asJson,
|
|
@@ -1989,7 +2055,7 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1989
2055
|
const id = videoIdFromUrl(url);
|
|
1990
2056
|
const baseline = proveSavedLearnerBaseline({
|
|
1991
2057
|
cwd,
|
|
1992
|
-
applyRel: id ?
|
|
2058
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
1993
2059
|
lesson: saved.lesson,
|
|
1994
2060
|
output,
|
|
1995
2061
|
json: deps.json === true,
|
|
@@ -2129,10 +2195,12 @@ function parseSearchStdout(stdout = '') {
|
|
|
2129
2195
|
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
2130
2196
|
const trimmed = line.trim();
|
|
2131
2197
|
if (!trimmed || !trimmed.includes('|')) continue;
|
|
2198
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
2132
2199
|
const parts = trimmed.split(/\s*\|\s*/).map((part) => part.trim());
|
|
2133
2200
|
if (parts.length < 5) continue;
|
|
2134
2201
|
const url = parts[parts.length - 1];
|
|
2135
2202
|
if (!/^https?:\/\/(?:www\.)?(?:youtube\.com\/|youtu\.be\/)/i.test(url)) continue;
|
|
2203
|
+
if (!looksLikeFlatVideoId(videoIdFromUrl(url))) continue;
|
|
2136
2204
|
const row = {
|
|
2137
2205
|
title: parts[0] || '',
|
|
2138
2206
|
channel: parts[1] || '',
|