atris 3.30.12 → 3.31.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 +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +3 -1
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +7 -0
- package/ax +95 -3
- package/bin/atris.js +126 -153
- package/commands/autoland.js +319 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +1 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +519 -19
- package/commands/mission.js +3330 -290
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run.js +3 -3
- package/commands/strings.js +301 -0
- package/commands/task.js +575 -102
- package/commands/truth.js +170 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +283 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/next-moves.js +212 -6
- package/lib/pulse.js +74 -1
- package/lib/runner-command.js +20 -8
- package/lib/runs-prune.js +242 -0
- package/lib/task-proof.js +1 -1
- package/package.json +3 -3
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
package/lib/autoland.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Autoland: the owner approves the policy once, then certified work lands
|
|
4
|
+
// itself. Humans keep the genuinely irreversible lanes (the denied tags in
|
|
5
|
+
// lib/auto-accept-certified.js); everything else is verified, reversible,
|
|
6
|
+
// and receipted, so waiting for a human click adds latency, not safety.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_DIGEST_HOUR = 9;
|
|
14
|
+
const DEFAULT_ALARM_HOURS = 24;
|
|
15
|
+
|
|
16
|
+
function policyPath(root) {
|
|
17
|
+
return path.join(root, '.atris', 'policy', 'autoland.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function statePath(root) {
|
|
21
|
+
return path.join(root, '.atris', 'state', 'autoland.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(file, fallback) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeJson(file, value) {
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readPolicy(root) {
|
|
38
|
+
const policy = readJson(policyPath(root), null);
|
|
39
|
+
if (!policy || typeof policy !== 'object') return null;
|
|
40
|
+
return policy;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writePolicy(root, policy) {
|
|
44
|
+
writeJson(policyPath(root), policy);
|
|
45
|
+
return policy;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readState(root) {
|
|
49
|
+
const state = readJson(statePath(root), {});
|
|
50
|
+
if (!state.alerts || typeof state.alerts !== 'object') state.alerts = {};
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeState(root, state) {
|
|
55
|
+
writeJson(statePath(root), state);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The standing authorization cmdAutoAcceptCertified consults: live accepts
|
|
59
|
+
// are allowed without a per-run human confirmation only when the owner has
|
|
60
|
+
// flipped the policy on for this workspace.
|
|
61
|
+
function liveAcceptAuthorization(root = process.cwd()) {
|
|
62
|
+
const policy = readPolicy(root);
|
|
63
|
+
if (!policy || policy.enabled !== true) return { ok: false, reason: 'autoland_policy_off' };
|
|
64
|
+
const actor = String(policy.enabled_by || '').trim();
|
|
65
|
+
if (!actor) return { ok: false, reason: 'autoland_policy_missing_owner' };
|
|
66
|
+
return { ok: true, actor, policy: 'autoland', strictVerify: policy.strict_verify !== false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cronMarker(root) {
|
|
70
|
+
const slug = path.basename(root).replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase();
|
|
71
|
+
return `ATRIS_AUTOLAND_${slug}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildCronLine(root) {
|
|
75
|
+
const node = process.execPath;
|
|
76
|
+
const bin = path.join(root, 'bin', 'atris.js');
|
|
77
|
+
const cliBin = fs.existsSync(bin) ? bin : 'atris';
|
|
78
|
+
const logDir = path.join(os.homedir(), '.atris', 'overnight');
|
|
79
|
+
const logFile = path.join(logDir, `autoland-${path.basename(root)}.log`);
|
|
80
|
+
const invoke = cliBin.endsWith('.js') ? `${node} ${cliBin}` : cliBin;
|
|
81
|
+
return `14 * * * * cd ${root} && ${invoke} autoland tick >> ${logFile} 2>&1 # ${cronMarker(root)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readCrontab() {
|
|
85
|
+
const result = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
|
|
86
|
+
return result.status === 0 ? String(result.stdout || '') : '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function applyCrontab(content) {
|
|
90
|
+
const result = spawnSync('crontab', ['-'], { input: content, encoding: 'utf8', timeout: 10000 });
|
|
91
|
+
return result.status === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cronInstalled(root) {
|
|
95
|
+
return readCrontab().includes(cronMarker(root));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function installCron(root) {
|
|
99
|
+
fs.mkdirSync(path.join(os.homedir(), '.atris', 'overnight'), { recursive: true });
|
|
100
|
+
const marker = cronMarker(root);
|
|
101
|
+
const kept = readCrontab().split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
|
|
102
|
+
const next = `${kept.join('\n')}${kept.length ? '\n' : ''}${buildCronLine(root)}\n`;
|
|
103
|
+
return applyCrontab(next);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function uninstallCron(root) {
|
|
107
|
+
const marker = cronMarker(root);
|
|
108
|
+
const kept = readCrontab().split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
|
|
109
|
+
return applyCrontab(kept.length ? `${kept.join('\n')}\n` : '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function certifiedAtMs(task) {
|
|
113
|
+
const iso = task.metadata?.agent_certified_at;
|
|
114
|
+
if (iso) {
|
|
115
|
+
const ms = Date.parse(iso);
|
|
116
|
+
if (Number.isFinite(ms)) return ms;
|
|
117
|
+
}
|
|
118
|
+
return Number(task.updated_at) || Date.now();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function waitingHours(task, now = Date.now()) {
|
|
122
|
+
return Math.floor((now - certifiedAtMs(task)) / 3600000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Tasks that are certified and pending — i.e. finished work sitting on the
|
|
126
|
+
// human side of the fence, whatever the reason.
|
|
127
|
+
function waitingOnHuman(tasks, now = Date.now()) {
|
|
128
|
+
return (tasks || [])
|
|
129
|
+
.filter((t) => t.status === 'review')
|
|
130
|
+
.filter((t) => String(t.review?.approval_status || t.metadata?.approval_status || 'pending') === 'pending')
|
|
131
|
+
.filter((t) => t.review?.agent_certified === true || t.metadata?.agent_certified === true)
|
|
132
|
+
.map((t) => ({
|
|
133
|
+
ref: t.display_id || t.legacy_ref || t.id,
|
|
134
|
+
title: String(t.title || '').slice(0, 90),
|
|
135
|
+
tag: String(t.tag || ''),
|
|
136
|
+
hours: waitingHours(t, now),
|
|
137
|
+
}))
|
|
138
|
+
.sort((a, b) => b.hours - a.hours);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function acceptedInLastDay(tasks, now = Date.now()) {
|
|
142
|
+
const auto = [];
|
|
143
|
+
const human = [];
|
|
144
|
+
for (const t of tasks || []) {
|
|
145
|
+
const acceptedAt = Date.parse(t.metadata?.accepted_at || '') || 0;
|
|
146
|
+
if (!acceptedAt || now - acceptedAt > 24 * 3600000) continue;
|
|
147
|
+
const entry = {
|
|
148
|
+
ref: t.display_id || t.legacy_ref || t.id,
|
|
149
|
+
title: String(t.title || '').slice(0, 120),
|
|
150
|
+
happened: String(t.review?.landing?.happened || t.metadata?.landing_happened || '').slice(0, 300),
|
|
151
|
+
member: String(t.claimed_by || t.assigned_to || '').trim(),
|
|
152
|
+
};
|
|
153
|
+
if (t.metadata?.auto_accepted_at) auto.push(entry);
|
|
154
|
+
else human.push(entry);
|
|
155
|
+
}
|
|
156
|
+
return { auto, human };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function plur(n, word) {
|
|
160
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cutAtWord(text, max) {
|
|
164
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
165
|
+
if (clean.length <= max) return clean;
|
|
166
|
+
const cut = clean.slice(0, max);
|
|
167
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
168
|
+
return `${cut.slice(0, lastSpace > max * 0.6 ? lastSpace : max)}...`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// One line per piece, in the work's own words: the landing sentence beats
|
|
172
|
+
// the task title, because titles are written for the queue, not for a text.
|
|
173
|
+
function digestLine(item) {
|
|
174
|
+
const happened = String(item.happened || '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
175
|
+
const title = String(item.title || '').replace(/^Mission XP:\s*/i, '');
|
|
176
|
+
const source = happened.length >= 12 ? happened : title;
|
|
177
|
+
const line = cutAtWord(source.replace(/[.\s]+$/, ''), 72);
|
|
178
|
+
return line.charAt(0).toLowerCase() + line.slice(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The mission loop files a check-off every time it stops instead of
|
|
182
|
+
// inventing work; those all say the same thing, so they collapse into one line.
|
|
183
|
+
function isCleanStop(item) {
|
|
184
|
+
return /no concrete follow-up|did not start fake work|stopped with reason/i.test(String(item.happened || ''))
|
|
185
|
+
|| /^(Mission XP:\s*)?Decide and start the next useful mission/i.test(String(item.title || ''));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Short enough for one iMessage, plain enough to read over coffee — and it
|
|
189
|
+
// names the work, because "7 things landed" tells the score, not the story.
|
|
190
|
+
function composeDigest({ accepted, waiting, landed, project }) {
|
|
191
|
+
const lines = [];
|
|
192
|
+
lines.push(`atris ${project} daily`);
|
|
193
|
+
const autoCount = accepted.auto.length;
|
|
194
|
+
const humanCount = accepted.human.length;
|
|
195
|
+
if (autoCount > 0) {
|
|
196
|
+
lines.push('got their final sign-off on their own (checked twice, proof on file):');
|
|
197
|
+
const stops = accepted.auto.filter(isCleanStop);
|
|
198
|
+
const work = accepted.auto.filter((item) => !isCleanStop(item));
|
|
199
|
+
for (const item of work.slice(0, 4)) lines.push(`- ${digestLine(item) || item.ref}`);
|
|
200
|
+
if (work.length > 4) lines.push(`- and ${work.length - 4} more`);
|
|
201
|
+
if (stops.length > 0) lines.push(`- ${plur(stops.length, 'check')} that the loop stops cleanly instead of inventing work`);
|
|
202
|
+
}
|
|
203
|
+
if (humanCount > 0) lines.push(`you approved ${plur(humanCount, 'piece')} yourself`);
|
|
204
|
+
if (autoCount + humanCount === 0) lines.push('nothing finished in the last day');
|
|
205
|
+
const byMember = new Map();
|
|
206
|
+
for (const item of [...accepted.auto, ...accepted.human]) {
|
|
207
|
+
if (!item.member || isCleanStop(item)) continue;
|
|
208
|
+
byMember.set(item.member, (byMember.get(item.member) || 0) + 1);
|
|
209
|
+
}
|
|
210
|
+
if (byMember.size > 0) {
|
|
211
|
+
lines.push(`workers: ${Array.from(byMember, ([who, n]) => `${who} landed ${n}`).join(', ')}`);
|
|
212
|
+
}
|
|
213
|
+
if (waiting.length > 0) {
|
|
214
|
+
lines.push(`waiting on you: ${waiting.length} (oldest ${waiting[0].hours}h) — atris task reviews`);
|
|
215
|
+
} else {
|
|
216
|
+
lines.push('nothing is waiting on you');
|
|
217
|
+
}
|
|
218
|
+
if (landed && landed.branches > 0) {
|
|
219
|
+
lines.push(`${plur(landed.branches, 'piece')} of work still in the air${landed.due > 0 ? `, ${landed.due} overdue` : ''} — atris land`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('the full story: atris autoland digest');
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function composeAlarm({ waiting, project, alarmHours }) {
|
|
226
|
+
const worst = waiting[0];
|
|
227
|
+
return [
|
|
228
|
+
`atris ${project}: ${plur(waiting.length, 'piece')} of finished work ${waiting.length === 1 ? 'has' : 'have'} been waiting on you for over ${alarmHours}h`,
|
|
229
|
+
`oldest: ${worst.ref} (${worst.hours}h) — ${worst.title}`,
|
|
230
|
+
'approve or bounce: atris task reviews',
|
|
231
|
+
].join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Alarm dedupe: a task pings at most once per alarm window.
|
|
235
|
+
function dueForAlarm(waiting, state, { alarmHours = DEFAULT_ALARM_HOURS, now = Date.now() } = {}) {
|
|
236
|
+
const due = [];
|
|
237
|
+
for (const item of waiting) {
|
|
238
|
+
if (item.hours < alarmHours) continue;
|
|
239
|
+
const lastAlert = Date.parse(state.alerts[item.ref] || '') || 0;
|
|
240
|
+
if (now - lastAlert < alarmHours * 3600000) continue;
|
|
241
|
+
due.push(item);
|
|
242
|
+
}
|
|
243
|
+
return due;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function markAlerted(state, items, now = Date.now()) {
|
|
247
|
+
for (const item of items) state.alerts[item.ref] = new Date(now).toISOString();
|
|
248
|
+
return state;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function sendImessage(root, to, text) {
|
|
252
|
+
const bin = path.join(root, 'bin', 'atris.js');
|
|
253
|
+
const argv = fs.existsSync(bin)
|
|
254
|
+
? [process.execPath, [bin, 'imessage', 'send', '--to', to, '--text', text, '--approved']]
|
|
255
|
+
: ['atris', ['imessage', 'send', '--to', to, '--text', text, '--approved']];
|
|
256
|
+
const result = spawnSync(argv[0], argv[1], { cwd: root, encoding: 'utf8', timeout: 30000 });
|
|
257
|
+
return { ok: result.status === 0, output: String(result.stdout || result.stderr || '').trim().slice(0, 300) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
DEFAULT_ALARM_HOURS,
|
|
262
|
+
DEFAULT_DIGEST_HOUR,
|
|
263
|
+
acceptedInLastDay,
|
|
264
|
+
buildCronLine,
|
|
265
|
+
composeAlarm,
|
|
266
|
+
composeDigest,
|
|
267
|
+
cronInstalled,
|
|
268
|
+
cronMarker,
|
|
269
|
+
dueForAlarm,
|
|
270
|
+
installCron,
|
|
271
|
+
liveAcceptAuthorization,
|
|
272
|
+
markAlerted,
|
|
273
|
+
policyPath,
|
|
274
|
+
readPolicy,
|
|
275
|
+
readState,
|
|
276
|
+
sendImessage,
|
|
277
|
+
statePath,
|
|
278
|
+
uninstallCron,
|
|
279
|
+
waitingHours,
|
|
280
|
+
waitingOnHuman,
|
|
281
|
+
writePolicy,
|
|
282
|
+
writeState,
|
|
283
|
+
};
|
package/lib/context-gatherer.js
CHANGED
|
@@ -25,13 +25,6 @@ function hasContextProfile(root = process.cwd()) {
|
|
|
25
25
|
return Boolean(profile && String(profile.first_answer || '').trim());
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function isAtrisMetaQuestion(value) {
|
|
29
|
-
const text = String(value || '').trim().toLowerCase().replace(/[?.!]+$/g, '');
|
|
30
|
-
if (!text) return false;
|
|
31
|
-
return /^(what is|what's|whats|who is|who's|whos|explain|tell me about)\s+atris\b/.test(text)
|
|
32
|
-
|| /^atris\s+(help|overview|what is this|what are you)$/i.test(text);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
28
|
function compactText(value, max = 160) {
|
|
36
29
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
37
30
|
if (!text) return '';
|
|
@@ -177,7 +170,6 @@ module.exports = {
|
|
|
177
170
|
saveContextProfile,
|
|
178
171
|
createStarterTask,
|
|
179
172
|
shouldGatherContext,
|
|
180
|
-
isAtrisMetaQuestion,
|
|
181
173
|
renderPrompt,
|
|
182
174
|
starterTaskTitle,
|
|
183
175
|
inferDomain,
|