atris 3.35.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Live vitals page for the self-improvement metabolism. One warm dark page,
|
|
4
|
+
// sentences over metrics, waiting-on-you first. Regenerated by every pulse
|
|
5
|
+
// tick and by `atris improve --html`; the page reloads itself every minute.
|
|
6
|
+
//
|
|
7
|
+
// Aesthetic: night instrument journal. One display font (Fraunces), system
|
|
8
|
+
// mono for time stamps, ink tinted toward green, a single ember accent.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const HTML_RELATIVE_PATH = path.join('.atris', 'state', 'improve-vitals.html');
|
|
15
|
+
|
|
16
|
+
function vitalsHtmlPath(root = process.cwd()) {
|
|
17
|
+
return path.join(root, HTML_RELATIVE_PATH);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function esc(text) {
|
|
21
|
+
return String(text == null ? '' : text)
|
|
22
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function commitsSinceMidnight(root) {
|
|
26
|
+
try {
|
|
27
|
+
const r = spawnSync('git', ['-C', root, 'log', '--since=midnight', '--format=%s'], {
|
|
28
|
+
encoding: 'utf8', timeout: 10000,
|
|
29
|
+
});
|
|
30
|
+
if (r.status !== 0) return [];
|
|
31
|
+
return String(r.stdout || '').split('\n').map((s) => s.trim()).filter(Boolean).slice(0, 12);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function closedToday(cwd, now) {
|
|
38
|
+
try {
|
|
39
|
+
const close = require('../commands/close');
|
|
40
|
+
const today = now.toISOString().slice(0, 10);
|
|
41
|
+
return close.foldEvents(close.readEvents(cwd), { now })
|
|
42
|
+
.filter((f) => (f.status === 'closed' && String(f.closed_at || '').startsWith(today))
|
|
43
|
+
|| (f.status === 'dissolved' && String(f.dissolved_at || '').startsWith(today)))
|
|
44
|
+
.map((f) => ({ what: f.what, status: f.status }));
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function openLoops(cwd, now) {
|
|
51
|
+
try {
|
|
52
|
+
const close = require('../commands/close');
|
|
53
|
+
const flags = close.openFlags(cwd, { now });
|
|
54
|
+
const mine = flags.filter((f) => /^(you|me|operator|human|keshav)$/.test(f.owner));
|
|
55
|
+
const theirs = flags.filter((f) => !/^(you|me|operator|human|keshav)$/.test(f.owner));
|
|
56
|
+
return { mine, theirs };
|
|
57
|
+
} catch {
|
|
58
|
+
return { mine: [], theirs: [] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loopLine(flag) {
|
|
63
|
+
const age = flag.overdue ? `${flag.days_past_ttl} ${flag.days_past_ttl === 1 ? 'day' : 'days'} late` : `due in ${Math.max(0, Math.ceil((Date.parse(flag.due_at) - Date.now()) / 86400000))} days`;
|
|
64
|
+
return { what: flag.what, when: age, overdue: flag.overdue, lane: flag.lane };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderVitalsHtml(root = process.cwd(), deps = {}) {
|
|
68
|
+
const collect = deps.collectImproveVitals || require('../commands/improve').collectImproveVitals;
|
|
69
|
+
const now = deps.now ? new Date(deps.now) : new Date();
|
|
70
|
+
const vitals = collect({ workspace: root, now: now.toISOString() });
|
|
71
|
+
const sentences = ['heartbeat', 'exploit', 'explore', 'excrete', 'usage']
|
|
72
|
+
.map((k) => vitals[k] && vitals[k].sentence)
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
const loops = openLoops(root, now);
|
|
75
|
+
const done = closedToday(root, now);
|
|
76
|
+
const commits = commitsSinceMidnight(root);
|
|
77
|
+
const stamp = now.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit', month: 'short', day: 'numeric' });
|
|
78
|
+
|
|
79
|
+
const loopItem = (f) => {
|
|
80
|
+
const l = loopLine(f);
|
|
81
|
+
return `<li class="${l.overdue ? 'late' : ''}"><span class="lane">${esc(l.lane)}</span>${esc(l.what)}<span class="when">${esc(l.when)}</span></li>`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return `<!doctype html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="utf-8">
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
89
|
+
<meta http-equiv="refresh" content="60">
|
|
90
|
+
<title>atris is improving</title>
|
|
91
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
92
|
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,600&display=swap" rel="stylesheet">
|
|
93
|
+
<style>
|
|
94
|
+
:root {
|
|
95
|
+
--ink: oklch(22% 0.02 160);
|
|
96
|
+
--ink-deep: oklch(17% 0.02 160);
|
|
97
|
+
--paper: oklch(93% 0.015 90);
|
|
98
|
+
--paper-dim: oklch(72% 0.02 120);
|
|
99
|
+
--ember: oklch(72% 0.14 55);
|
|
100
|
+
--line: oklch(32% 0.02 150);
|
|
101
|
+
}
|
|
102
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
103
|
+
* { box-sizing: border-box; }
|
|
104
|
+
body {
|
|
105
|
+
margin: 0; min-height: 100vh; color: var(--paper);
|
|
106
|
+
background: radial-gradient(120% 90% at 15% 0%, var(--ink) 0%, var(--ink-deep) 70%);
|
|
107
|
+
font-family: 'Fraunces', georgia, serif; font-weight: 300;
|
|
108
|
+
font-size: clamp(17px, 1.6vw, 20px); line-height: 1.65;
|
|
109
|
+
}
|
|
110
|
+
main { max-width: 68ch; margin: 0 auto; padding: clamp(2rem, 6vw, 5rem) 1.4rem 4rem; }
|
|
111
|
+
h1 { font-weight: 600; font-size: clamp(2rem, 5vw, 3.2rem); line-height: 1.1; margin: 0 0 0.4rem; }
|
|
112
|
+
.stamp { font-family: ui-monospace, monospace; font-size: 0.72em; color: var(--paper-dim); margin-bottom: clamp(2rem, 5vw, 3.5rem); }
|
|
113
|
+
section { margin-bottom: clamp(2.2rem, 5vw, 3.2rem); }
|
|
114
|
+
h2 { font-weight: 600; font-size: 1.05em; margin: 0 0 0.7rem; color: var(--ember); }
|
|
115
|
+
p.vital { margin: 0.55rem 0; }
|
|
116
|
+
ul { list-style: none; margin: 0; padding: 0; }
|
|
117
|
+
li { padding: 0.55rem 0; border-top: 1px solid var(--line); display: flex; gap: 0.7rem; align-items: baseline; }
|
|
118
|
+
li:first-child { border-top: 0; }
|
|
119
|
+
li .lane { font-family: ui-monospace, monospace; font-size: 0.62em; color: var(--paper-dim); min-width: 4.5ch; }
|
|
120
|
+
li .when { margin-left: auto; font-family: ui-monospace, monospace; font-size: 0.68em; color: var(--paper-dim); white-space: nowrap; padding-left: 0.8rem; }
|
|
121
|
+
li.late .when { color: var(--ember); }
|
|
122
|
+
li.done { color: var(--paper-dim); }
|
|
123
|
+
.quiet { color: var(--paper-dim); }
|
|
124
|
+
footer { font-family: ui-monospace, monospace; font-size: 0.68em; color: var(--paper-dim); border-top: 1px solid var(--line); padding-top: 1rem; }
|
|
125
|
+
</style>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
<main>
|
|
129
|
+
<h1>atris is improving</h1>
|
|
130
|
+
<div class="stamp">as of ${esc(stamp)}, refreshes itself every minute</div>
|
|
131
|
+
|
|
132
|
+
<section>
|
|
133
|
+
<h2>the organism right now</h2>
|
|
134
|
+
${sentences.map((s) => `<p class="vital">${esc(s)}</p>`).join('\n ')}
|
|
135
|
+
</section>
|
|
136
|
+
|
|
137
|
+
<section>
|
|
138
|
+
<h2>waiting on you</h2>
|
|
139
|
+
${loops.mine.length ? `<ul>\n ${loops.mine.map(loopItem).join('\n ')}\n </ul>` : '<p class="quiet">nothing needs you right now.</p>'}
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
<section>
|
|
143
|
+
<h2>the system is carrying</h2>
|
|
144
|
+
${loops.theirs.length ? `<ul>\n ${loops.theirs.slice(0, 14).map(loopItem).join('\n ')}\n </ul>` : '<p class="quiet">no open loops on the system side.</p>'}
|
|
145
|
+
${loops.theirs.length > 14 ? `<p class="quiet">and ${loops.theirs.length - 14} more.</p>` : ''}
|
|
146
|
+
</section>
|
|
147
|
+
|
|
148
|
+
<section>
|
|
149
|
+
<h2>closed or shipped today</h2>
|
|
150
|
+
${(done.length || commits.length)
|
|
151
|
+
? `<ul>\n ${done.map((d) => `<li class="done"><span class="lane">${d.status === 'dissolved' ? 'let go' : 'closed'}</span>${esc(d.what)}</li>`).join('\n ')}
|
|
152
|
+
${commits.map((c) => `<li class="done"><span class="lane">landed</span>${esc(c)}</li>`).join('\n ')}\n </ul>`
|
|
153
|
+
: '<p class="quiet">nothing yet today.</p>'}
|
|
154
|
+
</section>
|
|
155
|
+
|
|
156
|
+
<footer>this page is written by the loop itself on every heartbeat.</footer>
|
|
157
|
+
</main>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeVitalsHtml(root = process.cwd(), deps = {}) {
|
|
164
|
+
const html = renderVitalsHtml(root, deps);
|
|
165
|
+
const file = vitalsHtmlPath(root);
|
|
166
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
167
|
+
fs.writeFileSync(file, html);
|
|
168
|
+
return file;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { renderVitalsHtml, writeVitalsHtml, vitalsHtmlPath };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const knownCommands = ['init', 'log', 'logs', 'wish', 'drill', 'dream', 'now', 'goal', 'wtf', 'orb', 'radar', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '_start', 'plan', 'do', 'review', 'release',
|
|
4
|
+
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
|
+
'clean', 'close', 'harvest', 'verify', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'bench', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'agents', 'probe', 'worktree', 'land', 'autoland', 'drive', 'aeo', 'slop', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail',
|
|
7
|
+
'github', 'vercel', 'supabase', 'linear', 'stripe', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
|
+
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'feed', 'brief'];
|
|
9
|
+
|
|
10
|
+
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
|
11
|
+
// adjacent transposition (e.g. "taks" -> "task") as a single edit, since
|
|
12
|
+
// transpositions are the most common command typo.
|
|
13
|
+
function editDistance(a, b) {
|
|
14
|
+
if (a === b) return 0;
|
|
15
|
+
const m = a.length;
|
|
16
|
+
const n = b.length;
|
|
17
|
+
if (m === 0) return n;
|
|
18
|
+
if (n === 0) return m;
|
|
19
|
+
// Full matrix so we can look back two rows for the transposition case.
|
|
20
|
+
const d = [];
|
|
21
|
+
for (let i = 0; i <= m; i++) d.push([i, ...new Array(n).fill(0)]);
|
|
22
|
+
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
23
|
+
for (let i = 1; i <= m; i++) {
|
|
24
|
+
for (let j = 1; j <= n; j++) {
|
|
25
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
26
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
27
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
28
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return d[m][n];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Given a mistyped command, return the closest known command if it's a near
|
|
36
|
+
// miss (small edit distance or clear prefix), else null. Helps a new person who
|
|
37
|
+
// typos `atris taks` get pointed at `task` instead of a confusing fallthrough.
|
|
38
|
+
function suggestCommand(input, commands = knownCommands) {
|
|
39
|
+
if (!input || typeof input !== 'string') return null;
|
|
40
|
+
const word = input.toLowerCase().trim();
|
|
41
|
+
if (!word) return null;
|
|
42
|
+
// Ignore internal/private commands (leading underscore) as suggestions.
|
|
43
|
+
const candidates = commands.filter((c) => !c.startsWith('_'));
|
|
44
|
+
let best = null;
|
|
45
|
+
let bestScore = Infinity;
|
|
46
|
+
for (const c of candidates) {
|
|
47
|
+
const d = editDistance(word, c);
|
|
48
|
+
// Threshold scales gently with length: allow 1 edit for short words, 2 for longer.
|
|
49
|
+
const maxEdits = word.length <= 4 ? 1 : 2;
|
|
50
|
+
if (d <= maxEdits && d < bestScore) {
|
|
51
|
+
best = c;
|
|
52
|
+
bestScore = d;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { knownCommands, suggestCommand, editDistance };
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RECEIPT_LIMIT = 30;
|
|
7
|
+
const DEFAULT_FINDING_FRESHNESS_MS = 2 * 60 * 60 * 1000;
|
|
8
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const AUTH_FAILURES = [
|
|
10
|
+
{ key: 'http-401', pattern: /\b401\b/i, label: '401' },
|
|
11
|
+
{ key: 'token-expired', pattern: /token expired/i, label: 'Token expired' },
|
|
12
|
+
{ key: 'signature-verification-failed', pattern: /signature verification failed/i, label: 'Signature verification failed' },
|
|
13
|
+
];
|
|
14
|
+
const TERMINAL_MISSION_STATUSES = new Set(['completed', 'complete', 'done', 'stopped', 'cancelled', 'canceled']);
|
|
15
|
+
const HEADLESS_CAPABLE_RUNNERS = new Set([
|
|
16
|
+
'auto',
|
|
17
|
+
'claude',
|
|
18
|
+
'atris2',
|
|
19
|
+
'atris-fast',
|
|
20
|
+
'codex',
|
|
21
|
+
'cursor',
|
|
22
|
+
'devin',
|
|
23
|
+
'hermes',
|
|
24
|
+
'hermes-agent',
|
|
25
|
+
]);
|
|
26
|
+
const FINDING_PRIORITY = {
|
|
27
|
+
repeated_auth_failure: 500,
|
|
28
|
+
repeated_tick_error: 400,
|
|
29
|
+
stale_mission_lock: 350,
|
|
30
|
+
repeated_pause_reason: 300,
|
|
31
|
+
zero_due_capable_missions: 200,
|
|
32
|
+
reward_flatline: 100,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function readJson(file) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJsonl(file) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(file, 'utf8')
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.map((line) => line.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((line) => {
|
|
50
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function timestamp(value) {
|
|
59
|
+
const ms = Date.parse(String(value || ''));
|
|
60
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function objectTimestamp(value) {
|
|
64
|
+
if (!value || typeof value !== 'object') return null;
|
|
65
|
+
const fields = ['ts', 'at', 'finished_at', 'updated_at', 'created_at', 'started_at', 'timestamp'];
|
|
66
|
+
for (const field of fields) {
|
|
67
|
+
const parsed = timestamp(value[field]);
|
|
68
|
+
if (parsed) return parsed;
|
|
69
|
+
}
|
|
70
|
+
if (value.result && typeof value.result === 'object') return objectTimestamp(value.result);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function occurrenceRange(occurrences, fallback = null) {
|
|
75
|
+
const times = occurrences.map((item) => timestamp(item.ts)).filter(Boolean).sort();
|
|
76
|
+
return {
|
|
77
|
+
first_seen: times[0] || fallback,
|
|
78
|
+
last_seen: times[times.length - 1] || fallback,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function occurrenceIsFresh(range, nowIso, freshnessMs) {
|
|
83
|
+
if (!range.last_seen) return true;
|
|
84
|
+
const nowMs = Date.parse(nowIso);
|
|
85
|
+
const lastSeenMs = Date.parse(range.last_seen);
|
|
86
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(lastSeenMs)) return true;
|
|
87
|
+
return nowMs - lastSeenMs <= freshnessMs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function latestMissionRecords(rows) {
|
|
91
|
+
const latest = new Map();
|
|
92
|
+
rows.forEach((row, index) => {
|
|
93
|
+
if (!row || typeof row !== 'object') return;
|
|
94
|
+
const mission = row.mission && typeof row.mission === 'object' ? row.mission : row;
|
|
95
|
+
const id = String(mission.id || row.mission_id || `row-${index}`);
|
|
96
|
+
latest.set(id, mission);
|
|
97
|
+
});
|
|
98
|
+
return [...latest.values()];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadMissionReceipts(root, limit = DEFAULT_RECEIPT_LIMIT) {
|
|
102
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
103
|
+
let names = [];
|
|
104
|
+
try {
|
|
105
|
+
names = fs.readdirSync(runsDir).filter((name) => /^mission-.*\.json$/i.test(name));
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
return names
|
|
110
|
+
.map((name) => {
|
|
111
|
+
const file = path.join(runsDir, name);
|
|
112
|
+
const receipt = readJson(file);
|
|
113
|
+
return receipt ? { name, file, receipt, ts: objectTimestamp(receipt) } : null;
|
|
114
|
+
})
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.sort((a, b) => String(a.ts || a.name).localeCompare(String(b.ts || b.name)))
|
|
117
|
+
.slice(-limit);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function walk(value, visit, trail = []) {
|
|
121
|
+
if (!value || typeof value !== 'object') return;
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
value.forEach((item, index) => walk(item, visit, [...trail, index]));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
visit(value, trail);
|
|
127
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
128
|
+
if (child && typeof child === 'object') walk(child, visit, [...trail, key]);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function receiptFailureSignals(entry) {
|
|
133
|
+
const signals = [];
|
|
134
|
+
walk(entry.receipt, (value, trail) => {
|
|
135
|
+
// A long-running mission rewrites one receipt on every tick, re-stamping
|
|
136
|
+
// its top-level time, so a failed tick from hours ago looks fresh forever
|
|
137
|
+
// if we date it by the receipt. Prefer the failing node's own timestamp
|
|
138
|
+
// (started_at / finished_at) and fall back to the receipt only when the
|
|
139
|
+
// node carries none.
|
|
140
|
+
const signalTs = objectTimestamp(value) || entry.ts;
|
|
141
|
+
if (typeof value.pause_reason === 'string' && value.pause_reason.trim()) {
|
|
142
|
+
signals.push({ type: 'pause_reason', value: value.pause_reason.trim(), trail, ts: signalTs });
|
|
143
|
+
}
|
|
144
|
+
const status = String(value.status || '').toLowerCase();
|
|
145
|
+
const tickContext = trail.some((part) => part === 'tick' || part === 'ticks');
|
|
146
|
+
const errored = ['error', 'errored', 'failed', 'failure'].includes(status)
|
|
147
|
+
|| value.errored === true
|
|
148
|
+
|| value.failed === true
|
|
149
|
+
|| (tickContext && (value.error != null || value.error_message != null));
|
|
150
|
+
if (!errored) return;
|
|
151
|
+
const detail = value.error || value.error_message || value.reason || value.message || status;
|
|
152
|
+
const text = typeof detail === 'string' ? detail.trim() : JSON.stringify(detail);
|
|
153
|
+
if (text) signals.push({ type: 'tick_error', value: text, trail, ts: signalTs });
|
|
154
|
+
});
|
|
155
|
+
const unique = new Map();
|
|
156
|
+
for (const signal of signals) unique.set(`${signal.type}\0${signal.value}`, signal);
|
|
157
|
+
return [...unique.values()];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function repeatedFailureFindings(receipts, nowIso, freshnessMs) {
|
|
161
|
+
const groups = new Map();
|
|
162
|
+
for (const entry of receipts) {
|
|
163
|
+
for (const signal of receiptFailureSignals(entry)) {
|
|
164
|
+
const key = `${signal.type}\0${signal.value}`;
|
|
165
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
166
|
+
groups.get(key).push({ ts: signal.ts || entry.ts, receipt: path.basename(entry.file), trail: signal.trail });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const byType = new Map();
|
|
171
|
+
for (const [key, occurrences] of groups) {
|
|
172
|
+
if (occurrences.length < 2) continue;
|
|
173
|
+
const range = occurrenceRange(occurrences);
|
|
174
|
+
if (!occurrenceIsFresh(range, nowIso, freshnessMs)) continue;
|
|
175
|
+
const [type, value] = key.split('\0');
|
|
176
|
+
const candidate = { type, value, occurrences, range };
|
|
177
|
+
const current = byType.get(type);
|
|
178
|
+
if (!current || occurrences.length > current.occurrences.length
|
|
179
|
+
|| (occurrences.length === current.occurrences.length && value.localeCompare(current.value) < 0)) {
|
|
180
|
+
byType.set(type, candidate);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return [...byType.values()].map(({ type, value, occurrences, range }) => {
|
|
185
|
+
const kind = type === 'pause_reason' ? 'repeated_pause_reason' : 'repeated_tick_error';
|
|
186
|
+
const label = type === 'pause_reason' ? 'pause reason' : 'tick error';
|
|
187
|
+
return {
|
|
188
|
+
kind,
|
|
189
|
+
evidence: {
|
|
190
|
+
value,
|
|
191
|
+
receipts: occurrences.map((item) => item.receipt),
|
|
192
|
+
detail: `the same ${label} appeared in ${occurrences.length} recent mission receipts`,
|
|
193
|
+
},
|
|
194
|
+
count: occurrences.length,
|
|
195
|
+
...range,
|
|
196
|
+
suggested_mission: {
|
|
197
|
+
objective: `repair the improve loop's repeated ${label}: ${value}`,
|
|
198
|
+
verifier: `atris improve doctor --check ${kind}`,
|
|
199
|
+
owner: 'auto-improver',
|
|
200
|
+
cadence: '15m',
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function authFailureFindings(receipts, nowIso, freshnessMs) {
|
|
207
|
+
const matches = AUTH_FAILURES.map((signature) => ({ signature, occurrences: [] }));
|
|
208
|
+
for (const entry of receipts) {
|
|
209
|
+
let text = '';
|
|
210
|
+
try { text = JSON.stringify(entry.receipt); } catch { text = ''; }
|
|
211
|
+
for (const match of matches) {
|
|
212
|
+
if (match.signature.pattern.test(text)) {
|
|
213
|
+
match.occurrences.push({ ts: entry.ts, receipt: path.basename(entry.file) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const repeated = matches.filter((match) => {
|
|
218
|
+
if (match.occurrences.length < 2) return false;
|
|
219
|
+
match.range = occurrenceRange(match.occurrences);
|
|
220
|
+
return occurrenceIsFresh(match.range, nowIso, freshnessMs);
|
|
221
|
+
})
|
|
222
|
+
.sort((a, b) => b.occurrences.length - a.occurrences.length || a.signature.key.localeCompare(b.signature.key))[0];
|
|
223
|
+
if (!repeated) return [];
|
|
224
|
+
return [{
|
|
225
|
+
kind: 'repeated_auth_failure',
|
|
226
|
+
evidence: {
|
|
227
|
+
signature: repeated.signature.label,
|
|
228
|
+
receipts: repeated.occurrences.map((item) => item.receipt),
|
|
229
|
+
detail: `${repeated.signature.label} appeared in ${repeated.occurrences.length} recent mission receipts`,
|
|
230
|
+
},
|
|
231
|
+
count: repeated.occurrences.length,
|
|
232
|
+
...repeated.range,
|
|
233
|
+
suggested_mission: {
|
|
234
|
+
objective: `repair repeated improve-loop authentication failures for ${repeated.signature.label}`,
|
|
235
|
+
verifier: 'atris improve doctor --check repeated_auth_failure',
|
|
236
|
+
owner: 'auto-improver',
|
|
237
|
+
cadence: '15m',
|
|
238
|
+
},
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function rewardFlatlineFinding(rows, nowIso) {
|
|
243
|
+
const nowMs = Date.parse(nowIso);
|
|
244
|
+
const since = nowMs - (7 * DAY_MS);
|
|
245
|
+
const recent = rows
|
|
246
|
+
.map((row) => ({ row, ts: objectTimestamp(row), reward: Number(row && row.reward) }))
|
|
247
|
+
.filter((item) => item.ts && Date.parse(item.ts) >= since && Date.parse(item.ts) <= nowMs && Number.isFinite(item.reward));
|
|
248
|
+
if (recent.length < 2 || recent.some((item) => item.reward > 0)) return [];
|
|
249
|
+
const occurrences = recent.map((item) => ({ ts: item.ts }));
|
|
250
|
+
return [{
|
|
251
|
+
kind: 'reward_flatline',
|
|
252
|
+
evidence: {
|
|
253
|
+
rewards: recent.map((item) => item.reward),
|
|
254
|
+
window_days: 7,
|
|
255
|
+
detail: `${recent.length} recent scorecards had no positive reward`,
|
|
256
|
+
},
|
|
257
|
+
count: recent.length,
|
|
258
|
+
...occurrenceRange(occurrences, nowIso),
|
|
259
|
+
suggested_mission: {
|
|
260
|
+
objective: 'repair the improve loop reward flatline and prove one positive scorecard',
|
|
261
|
+
verifier: 'atris improve doctor --check reward_flatline',
|
|
262
|
+
owner: 'auto-improver',
|
|
263
|
+
cadence: '15m',
|
|
264
|
+
},
|
|
265
|
+
}];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isOpenMission(mission) {
|
|
269
|
+
return mission && !TERMINAL_MISSION_STATUSES.has(String(mission.status || 'planning').toLowerCase());
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isDueCapableMission(mission) {
|
|
273
|
+
const cadence = String(mission && mission.cadence || 'manual').trim().toLowerCase();
|
|
274
|
+
const runner = String(mission && mission.runner || 'manual').trim().toLowerCase();
|
|
275
|
+
return isOpenMission(mission)
|
|
276
|
+
&& cadence !== 'manual'
|
|
277
|
+
&& cadence !== ''
|
|
278
|
+
&& mission.always_on === true
|
|
279
|
+
&& HEADLESS_CAPABLE_RUNNERS.has(runner);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function zeroDueCapableFinding(missions, nowIso) {
|
|
283
|
+
const open = latestMissionRecords(missions).filter(isOpenMission);
|
|
284
|
+
if (open.some(isDueCapableMission)) return [];
|
|
285
|
+
const occurrences = open.map((mission) => ({ ts: objectTimestamp(mission) }));
|
|
286
|
+
return [{
|
|
287
|
+
kind: 'zero_due_capable_missions',
|
|
288
|
+
evidence: {
|
|
289
|
+
open_missions: open.length,
|
|
290
|
+
cadences: [...new Set(open.map((mission) => String(mission.cadence || 'manual')))].sort(),
|
|
291
|
+
runners: [...new Set(open.map((mission) => String(mission.runner || 'manual')))].sort(),
|
|
292
|
+
detail: 'no open mission is always on with a non-manual cadence and a headless-capable runner',
|
|
293
|
+
},
|
|
294
|
+
count: open.length,
|
|
295
|
+
...occurrenceRange(occurrences, nowIso),
|
|
296
|
+
suggested_mission: {
|
|
297
|
+
objective: 'restore a due-capable headless mission for the improve loop',
|
|
298
|
+
verifier: 'atris improve doctor --check zero_due_capable_missions',
|
|
299
|
+
owner: 'auto-improver',
|
|
300
|
+
cadence: '15m',
|
|
301
|
+
},
|
|
302
|
+
}];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function missionLockPidAlive(pid) {
|
|
306
|
+
try {
|
|
307
|
+
process.kill(pid, 0);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return error && error.code !== 'ESRCH';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function staleMissionLockFinding(root, nowIso, pidAlive = missionLockPidAlive) {
|
|
315
|
+
const stateDir = path.join(root, '.atris', 'state');
|
|
316
|
+
let names = [];
|
|
317
|
+
try {
|
|
318
|
+
names = fs.readdirSync(stateDir).filter((name) => /^mission-.*\.lock$/i.test(name)).sort();
|
|
319
|
+
} catch {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const stale = [];
|
|
323
|
+
for (const name of names) {
|
|
324
|
+
const file = path.join(stateDir, name);
|
|
325
|
+
let lock = null;
|
|
326
|
+
try { lock = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
327
|
+
const pid = Number(lock && lock.pid);
|
|
328
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
329
|
+
stale.push({ lock: name, reason: 'unparseable', pid: null });
|
|
330
|
+
} else if (!pidAlive(pid)) {
|
|
331
|
+
stale.push({ lock: name, reason: 'dead-pid', pid });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!stale.length) return [];
|
|
335
|
+
return [{
|
|
336
|
+
kind: 'stale_mission_lock',
|
|
337
|
+
evidence: {
|
|
338
|
+
locks: stale,
|
|
339
|
+
detail: `${stale.length} mission lock${stale.length === 1 ? '' : 's'} cannot belong to a live process`,
|
|
340
|
+
},
|
|
341
|
+
count: stale.length,
|
|
342
|
+
first_seen: nowIso,
|
|
343
|
+
last_seen: nowIso,
|
|
344
|
+
suggested_mission: {
|
|
345
|
+
objective: 'clear stale mission locks left by dead runs',
|
|
346
|
+
verifier: 'atris improve doctor --check stale_mission_lock',
|
|
347
|
+
owner: 'auto-improver',
|
|
348
|
+
cadence: '15m',
|
|
349
|
+
},
|
|
350
|
+
}];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const REWARD_PRESSURE_WINDOW_MS = DAY_MS;
|
|
354
|
+
const REWARD_PRESSURE_PER_ROW = 10;
|
|
355
|
+
const REWARD_PRESSURE_CAP = 100;
|
|
356
|
+
|
|
357
|
+
// Count scorecard rows in the last 24h whose numeric reward is <= 0. This is
|
|
358
|
+
// the same recency window shape as rewardFlatlineFinding, just 24h instead
|
|
359
|
+
// of 7d: a tighter, more urgent signal for "the loop is bleeding right now."
|
|
360
|
+
function nonPositiveRewardPressure(scorecards, nowIso) {
|
|
361
|
+
const nowMs = Date.parse(nowIso);
|
|
362
|
+
const since = nowMs - REWARD_PRESSURE_WINDOW_MS;
|
|
363
|
+
return scorecards
|
|
364
|
+
.map((row) => ({ ts: objectTimestamp(row), reward: Number(row && row.reward) }))
|
|
365
|
+
.filter((item) => item.ts && Date.parse(item.ts) >= since && Date.parse(item.ts) <= nowMs
|
|
366
|
+
&& Number.isFinite(item.reward) && item.reward <= 0)
|
|
367
|
+
.length;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function applyRewardPressure(findings, rewardPressure) {
|
|
371
|
+
return findings.map((finding) => ({
|
|
372
|
+
...finding,
|
|
373
|
+
reward_pressure: rewardPressure,
|
|
374
|
+
effective_priority: (FINDING_PRIORITY[finding.kind] || 0) + Math.min(rewardPressure * REWARD_PRESSURE_PER_ROW, REWARD_PRESSURE_CAP),
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function orderFindings(findings) {
|
|
379
|
+
return [...findings].sort((a, b) => (b.effective_priority ?? (FINDING_PRIORITY[b.kind] || 0)) - (a.effective_priority ?? (FINDING_PRIORITY[a.kind] || 0))
|
|
380
|
+
|| b.count - a.count
|
|
381
|
+
|| a.kind.localeCompare(b.kind));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function scanLoopReceipts({ root = process.cwd(), now = new Date(), freshnessMs = DEFAULT_FINDING_FRESHNESS_MS } = {}) {
|
|
385
|
+
const parsedNow = new Date(now);
|
|
386
|
+
const nowIso = Number.isFinite(parsedNow.getTime()) ? parsedNow.toISOString() : new Date().toISOString();
|
|
387
|
+
const receipts = loadMissionReceipts(root, DEFAULT_RECEIPT_LIMIT);
|
|
388
|
+
const scorecards = readJsonl(path.join(root, '.atris', 'state', 'scorecards.jsonl'));
|
|
389
|
+
const missions = readJsonl(path.join(root, '.atris', 'state', 'missions.jsonl'));
|
|
390
|
+
const rewardPressure = nonPositiveRewardPressure(scorecards, nowIso);
|
|
391
|
+
const findings = applyRewardPressure([
|
|
392
|
+
...authFailureFindings(receipts, nowIso, freshnessMs),
|
|
393
|
+
...repeatedFailureFindings(receipts, nowIso, freshnessMs),
|
|
394
|
+
...staleMissionLockFinding(root, nowIso),
|
|
395
|
+
...zeroDueCapableFinding(missions, nowIso),
|
|
396
|
+
...rewardFlatlineFinding(scorecards, nowIso),
|
|
397
|
+
], rewardPressure);
|
|
398
|
+
return orderFindings(findings);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
module.exports = {
|
|
402
|
+
scanLoopReceipts,
|
|
403
|
+
repeatedFailureFindings,
|
|
404
|
+
authFailureFindings,
|
|
405
|
+
rewardFlatlineFinding,
|
|
406
|
+
zeroDueCapableFinding,
|
|
407
|
+
staleMissionLockFinding,
|
|
408
|
+
isDueCapableMission,
|
|
409
|
+
latestMissionRecords,
|
|
410
|
+
orderFindings,
|
|
411
|
+
nonPositiveRewardPressure,
|
|
412
|
+
applyRewardPressure,
|
|
413
|
+
FINDING_PRIORITY,
|
|
414
|
+
DEFAULT_RECEIPT_LIMIT,
|
|
415
|
+
DEFAULT_FINDING_FRESHNESS_MS,
|
|
416
|
+
};
|