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.
Files changed (58) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +3 -1
  4. package/atris/atris.md +51 -19
  5. package/atris/skills/README.md +1 -0
  6. package/atris/skills/blocks/SKILL.md +134 -0
  7. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  8. package/atris/skills/youtube/SKILL.md +31 -11
  9. package/atris.md +7 -0
  10. package/ax +95 -3
  11. package/bin/atris.js +126 -153
  12. package/commands/autoland.js +319 -0
  13. package/commands/autopilot.js +94 -4
  14. package/commands/business.js +1 -1
  15. package/commands/clean.js +72 -9
  16. package/commands/codex-goal.js +72 -22
  17. package/commands/computer.js +48 -3
  18. package/commands/gm.js +1 -1
  19. package/commands/harvest.js +179 -0
  20. package/commands/init.js +1 -1
  21. package/commands/land.js +442 -0
  22. package/commands/loop-front.js +122 -4
  23. package/commands/member.js +519 -19
  24. package/commands/mission.js +3330 -290
  25. package/commands/play.js +1 -1
  26. package/commands/pulse.js +65 -7
  27. package/commands/run.js +3 -3
  28. package/commands/strings.js +301 -0
  29. package/commands/task.js +575 -102
  30. package/commands/truth.js +170 -0
  31. package/commands/xp.js +32 -8
  32. package/commands/youtube.js +72 -5
  33. package/decks/README.md +6 -12
  34. package/lib/auto-accept-certified.js +10 -0
  35. package/lib/autoland.js +283 -0
  36. package/lib/context-gatherer.js +0 -8
  37. package/lib/mission-artifact.js +504 -0
  38. package/lib/mission-room.js +846 -0
  39. package/lib/next-moves.js +212 -6
  40. package/lib/pulse.js +74 -1
  41. package/lib/runner-command.js +20 -8
  42. package/lib/runs-prune.js +242 -0
  43. package/lib/task-proof.js +1 -1
  44. package/package.json +3 -3
  45. package/decks/atris-seed-pitch-v3.json +0 -118
  46. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  47. package/decks/atris-seed-pitch-v5.json +0 -109
  48. package/decks/atris-seed-pitch-v6.json +0 -137
  49. package/decks/atris-seed-pitch-v7.json +0 -133
  50. package/decks/mark-pincus-narrative.json +0 -102
  51. package/decks/mark-pincus-sourcery.json +0 -94
  52. package/decks/yash-applied-compute-detailed.json +0 -150
  53. package/decks/yash-applied-compute-generalist.json +0 -82
  54. package/decks/yash-applied-compute-narrative.json +0 -54
  55. package/lib/ax-chat-input.js +0 -164
  56. package/lib/ax-goal.js +0 -307
  57. package/lib/ax-prefs.js +0 -63
  58. package/lib/ax-shimmer.js +0 -63
@@ -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
+ };
@@ -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,