atris 3.30.1 → 3.30.3
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 +6 -0
- package/atris/AGENTS.md +11 -0
- package/atris/CLAUDE.md +5 -0
- package/atris/atris.md +19 -4
- package/atris/policies/atris-design.md +71 -0
- package/atris/policies/design-seed.md +187 -0
- package/atris/skills/atris/SKILL.md +26 -3
- package/atris/skills/design/SKILL.md +3 -2
- package/atris/skills/loop/SKILL.md +5 -3
- package/atris/team/_template/MEMBER.md +19 -0
- package/atris.md +63 -23
- package/ax +1434 -1698
- package/bin/atris.js +5 -1
- package/commands/agent-spawn.js +2 -2
- package/commands/brain.js +92 -7
- package/commands/brainstorm.js +62 -22
- package/commands/business-sync.js +92 -8
- package/commands/business.js +13 -7
- package/commands/chat-scan.js +102 -0
- package/commands/computer.js +758 -15
- package/commands/deck.js +505 -105
- package/commands/init.js +6 -1
- package/commands/launchpad.js +638 -0
- package/commands/log-sync.js +44 -13
- package/commands/loop-front.js +290 -0
- package/commands/member.js +124 -3
- package/commands/mission.js +717 -32
- package/commands/pull.js +37 -28
- package/commands/pulse.js +11 -8
- package/commands/run.js +79 -39
- package/commands/sync.js +36 -17
- package/commands/task.js +342 -66
- package/commands/xp.js +3 -0
- package/commands/youtube.js +221 -7
- package/decks/README.md +89 -0
- package/decks/archetype-catalog.json +180 -0
- package/decks/atris-antislop-pitch.json +48 -0
- package/decks/atris-archetypes-v2.json +83 -0
- package/decks/atris-one-loop-pitch.json +80 -0
- package/decks/atris-seed-pitch-v3.json +118 -0
- package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
- package/decks/atris-seed-pitch-v5.json +109 -0
- package/decks/atris-seed-pitch-v6.json +137 -0
- package/decks/atris-seed-pitch-v7.json +133 -0
- package/decks/atris-single-shot-proof.json +74 -0
- package/decks/mark-pincus-narrative.json +102 -0
- package/decks/mark-pincus-sourcery.json +94 -0
- package/decks/yash-applied-compute-detailed.json +150 -0
- package/decks/yash-applied-compute-generalist.json +82 -0
- package/decks/yash-applied-compute-narrative.json +54 -0
- package/lib/ax-prefs.js +5 -12
- package/lib/chat-log-scan.js +377 -0
- package/lib/context-gatherer.js +35 -1
- package/lib/deck-compose.js +145 -0
- package/lib/deck-history.js +64 -0
- package/lib/deck-layout.js +169 -0
- package/lib/deck-review.js +431 -0
- package/lib/deck-schema.js +154 -0
- package/lib/file-ops.js +2 -2
- package/lib/functional-owner.js +189 -0
- package/lib/slides-deck.js +512 -58
- package/lib/task-db.js +109 -2
- package/package.json +2 -1
- package/templates/business-starter/team/START_HERE.md +12 -8
- package/utils/auth.js +4 -0
- package/utils/config.js +4 -0
- package/atris/atrisDev.md +0 -717
package/commands/init.js
CHANGED
|
@@ -671,6 +671,11 @@ Always-on agents should move proof-backed work to Review, complete their native
|
|
|
671
671
|
goal, then continue the mission loop with the next goal. They must not run
|
|
672
672
|
\`atris task accept\` or claim AgentXP unless a human approved the proof.
|
|
673
673
|
|
|
674
|
+
Task owners are functional or feature members, not engines. Use \`task-planner\`,
|
|
675
|
+
\`architect\`, \`mission-lead\`, \`validator\`, \`launcher\`, or a feature owner for
|
|
676
|
+
assignment; put coding agent models like Codex and Claude in the \`executed_by\`
|
|
677
|
+
section.
|
|
678
|
+
|
|
674
679
|
## Workflow
|
|
675
680
|
|
|
676
681
|
\`\`\`
|
|
@@ -699,7 +704,7 @@ member -> mission start --verify -> status --status active -> one bounded step -
|
|
|
699
704
|
- [ ] Use ASCII visuals for planning
|
|
700
705
|
- [ ] Check MAP.md before touching code
|
|
701
706
|
- [ ] Run \`atris task list\` or \`atris task next\` before picking work
|
|
702
|
-
- [ ] Claim tasks with \`atris task claim <id> --as <
|
|
707
|
+
- [ ] Claim tasks with \`atris task claim <id> --as <functional-member>\`
|
|
703
708
|
- [ ] Move agent-completed work to Review via \`atris task ready <id> --proof "..."\`
|
|
704
709
|
- [ ] Complete native Codex/Claude goals after proof is in Review, so always-on work can continue
|
|
705
710
|
- [ ] Only use \`atris task accept <id>\` when the human has approved the proof
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const ACTIVE_MISSION_STATUSES = new Set(['planning', 'running', 'ready']);
|
|
7
|
+
|
|
8
|
+
function hasFlag(args, name) {
|
|
9
|
+
return args.includes(name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readFlag(args, name, fallback = '') {
|
|
13
|
+
const prefix = `${name}=`;
|
|
14
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
15
|
+
const arg = String(args[i]);
|
|
16
|
+
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return String(args[i + 1]);
|
|
17
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
18
|
+
}
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readJson(file) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJsonLines(file) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.readFileSync(file, 'utf8')
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map(line => line.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map(line => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function inferActor() {
|
|
50
|
+
if (process.env.ATRIS_AGENT_ID) return process.env.ATRIS_AGENT_ID;
|
|
51
|
+
if (process.env.CODEX_SANDBOX) return 'codex';
|
|
52
|
+
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return 'claude';
|
|
53
|
+
if (process.env.CURSOR_AGENT) return 'cursor';
|
|
54
|
+
if (process.env.DEVIN_SESSION_ID) return 'devin';
|
|
55
|
+
return process.env.USER || 'operator';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clip(value, max = 120) {
|
|
59
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
60
|
+
if (text.length <= max) return text;
|
|
61
|
+
return `${text.slice(0, Math.max(0, max - 3)).trim()}...`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function taskRef(task) {
|
|
65
|
+
return task && (task.display_id || task.legacy_ref || task.id) || '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function taskUpdatedAt(task) {
|
|
69
|
+
return Number(task && task.updated_at || task && task.created_at || 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function taskReview(task) {
|
|
73
|
+
return task && (task.review || task.metadata || {}) || {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function taskAgentReviewPasses(task) {
|
|
77
|
+
const review = taskReview(task);
|
|
78
|
+
return Number(review.agent_review_pass_count || 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function taskIsAgentCertified(task) {
|
|
82
|
+
const review = taskReview(task);
|
|
83
|
+
return review.agent_certified === true || taskAgentReviewPasses(task) >= 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function taskNeedsAgentReview(task) {
|
|
87
|
+
if (!task || task.status !== 'review') return false;
|
|
88
|
+
return !taskIsAgentCertified(task);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function taskIsHumanGatedClaimed(task) {
|
|
92
|
+
if (!task || task.status !== 'claimed') return false;
|
|
93
|
+
const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
94
|
+
const text = [
|
|
95
|
+
task.title,
|
|
96
|
+
metadata.exit_condition,
|
|
97
|
+
metadata.verify,
|
|
98
|
+
metadata.proof_needed,
|
|
99
|
+
metadata.task_goal,
|
|
100
|
+
metadata.goal_objective,
|
|
101
|
+
metadata.stage_goal,
|
|
102
|
+
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
103
|
+
if (!text) return false;
|
|
104
|
+
if (/\bafter approval\b/i.test(text)) return true;
|
|
105
|
+
return /\b(human|operator)\b/i.test(text)
|
|
106
|
+
&& /\b(accept|approval|approve|confirm|choose|chooses|chosen|install|publish|release)\b/i.test(text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function taskIsOwnedByActor(task, actor) {
|
|
110
|
+
return String(task && task.claimed_by || '').toLowerCase() === String(actor || '').toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function taskReviewCommand(task) {
|
|
114
|
+
const review = task && task.review || {};
|
|
115
|
+
const verificationChat = review.verification_chat || {};
|
|
116
|
+
return verificationChat.command || `atris task review-chat ${taskRef(task)} --as codex-review`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function newest(tasks) {
|
|
120
|
+
return [...tasks].sort((a, b) => taskUpdatedAt(b) - taskUpdatedAt(a))[0] || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summarizeTasks(tasks) {
|
|
124
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
125
|
+
const counts = {
|
|
126
|
+
open: 0,
|
|
127
|
+
claimed: 0,
|
|
128
|
+
review: 0,
|
|
129
|
+
review_needs_agent: 0,
|
|
130
|
+
review_certified: 0,
|
|
131
|
+
failed: 0,
|
|
132
|
+
done_visible: 0,
|
|
133
|
+
};
|
|
134
|
+
for (const task of list) {
|
|
135
|
+
if (task.status === 'open') counts.open += 1;
|
|
136
|
+
else if (task.status === 'claimed') counts.claimed += 1;
|
|
137
|
+
else if (task.status === 'review') {
|
|
138
|
+
counts.review += 1;
|
|
139
|
+
if (taskNeedsAgentReview(task)) counts.review_needs_agent += 1;
|
|
140
|
+
if (taskIsAgentCertified(task)) counts.review_certified += 1;
|
|
141
|
+
} else if (task.status === 'failed') counts.failed += 1;
|
|
142
|
+
else if (task.status === 'done') counts.done_visible += 1;
|
|
143
|
+
}
|
|
144
|
+
return counts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function latestMissions(root) {
|
|
148
|
+
const file = path.join(root, '.atris', 'state', 'missions.jsonl');
|
|
149
|
+
const byId = new Map();
|
|
150
|
+
for (const record of readJsonLines(file)) {
|
|
151
|
+
const id = record.id || record.mission_id;
|
|
152
|
+
if (!id) continue;
|
|
153
|
+
byId.set(id, { ...record, id });
|
|
154
|
+
}
|
|
155
|
+
return [...byId.values()].filter(mission => ACTIVE_MISSION_STATUSES.has(String(mission.status || '')));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function missionNeedsTick(mission) {
|
|
159
|
+
if (!mission) return false;
|
|
160
|
+
const passed = mission.verifier_result && mission.verifier_result.passed === true;
|
|
161
|
+
return Boolean(mission.verifier) && !passed;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readBrain(root) {
|
|
165
|
+
const statusPath = path.join(root, 'atris', 'brain', 'STATUS.md');
|
|
166
|
+
const status = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : '';
|
|
167
|
+
const nextMoveMatch = status.match(/## Next Move\s+([\s\S]*?)(?:\n## |\n# |$)/);
|
|
168
|
+
const nextMove = nextMoveMatch
|
|
169
|
+
? nextMoveMatch[1].split(/\r?\n/).map(line => line.replace(/^[-\s]+/, '').trim()).find(Boolean) || ''
|
|
170
|
+
: '';
|
|
171
|
+
return {
|
|
172
|
+
status_path: fs.existsSync(statusPath) ? path.relative(root, statusPath) : null,
|
|
173
|
+
next_move: nextMove || null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readEndgame(root) {
|
|
178
|
+
const todoPath = path.join(root, 'atris', 'TODO.md');
|
|
179
|
+
if (!fs.existsSync(todoPath)) return null;
|
|
180
|
+
const text = fs.readFileSync(todoPath, 'utf8');
|
|
181
|
+
const slug = (text.match(/\*\*Slug:\*\*\s*([^\n]+)/) || [])[1];
|
|
182
|
+
const horizon = (text.match(/\*\*Horizon:\*\*\s*([^\n]+)/) || [])[1];
|
|
183
|
+
if (!slug && !horizon) return null;
|
|
184
|
+
return {
|
|
185
|
+
slug: slug ? slug.trim() : null,
|
|
186
|
+
horizon: horizon ? horizon.trim() : null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function loadTaskProjection(root) {
|
|
191
|
+
const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
192
|
+
const projection = readJson(projectionPath);
|
|
193
|
+
return {
|
|
194
|
+
projection_path: fs.existsSync(projectionPath) ? path.relative(root, projectionPath) : null,
|
|
195
|
+
projection,
|
|
196
|
+
tasks: projection && Array.isArray(projection.tasks) ? projection.tasks : [],
|
|
197
|
+
hidden_done_count: Number(projection && projection.surface && projection.surface.hidden_done_count || 0),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function chooseNextAction({ root, actor, tasks, missions, brain, endgame }) {
|
|
202
|
+
if (!fs.existsSync(path.join(root, 'atris'))) {
|
|
203
|
+
return {
|
|
204
|
+
kind: 'bootstrap',
|
|
205
|
+
label: 'Initialize this workspace',
|
|
206
|
+
command: 'atris init',
|
|
207
|
+
why: 'No atris/ directory exists yet.',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ownClaimed = newest(tasks.filter(task => (
|
|
212
|
+
task.status === 'claimed'
|
|
213
|
+
&& taskIsOwnedByActor(task, actor)
|
|
214
|
+
)));
|
|
215
|
+
if (ownClaimed) {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'continue_claimed_task',
|
|
218
|
+
label: `${taskRef(ownClaimed)} ${clip(ownClaimed.title, 90)}`,
|
|
219
|
+
command: `atris task step ${taskRef(ownClaimed)}`,
|
|
220
|
+
why: `${actor} already owns claimed work; finish or move it to Review before starting more.`,
|
|
221
|
+
task: compactTask(ownClaimed),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const mission = missions.find(missionNeedsTick);
|
|
226
|
+
if (mission) {
|
|
227
|
+
return {
|
|
228
|
+
kind: 'tick_mission',
|
|
229
|
+
label: clip(mission.objective || mission.id, 100),
|
|
230
|
+
command: `atris mission tick ${mission.id} --verify --complete-on-pass`,
|
|
231
|
+
why: 'A live mission has a verifier that has not passed yet.',
|
|
232
|
+
mission: compactMission(mission),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const reviewTask = newest(tasks.filter(taskNeedsAgentReview));
|
|
237
|
+
if (reviewTask) {
|
|
238
|
+
return {
|
|
239
|
+
kind: 'review_proof',
|
|
240
|
+
label: `${taskRef(reviewTask)} ${clip(reviewTask.title, 90)}`,
|
|
241
|
+
command: taskReviewCommand(reviewTask),
|
|
242
|
+
why: 'Review contains proof that still needs an agent verification pass.',
|
|
243
|
+
task: compactTask(reviewTask),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const certifiedReview = newest(tasks.filter(task => task.status === 'review' && taskIsAgentCertified(task)));
|
|
248
|
+
if (certifiedReview) {
|
|
249
|
+
return {
|
|
250
|
+
kind: 'human_accept_queue',
|
|
251
|
+
label: `${taskRef(certifiedReview)} ${clip(certifiedReview.title, 90)}`,
|
|
252
|
+
command: 'atris task reviews --limit 5',
|
|
253
|
+
why: 'Certified proof is waiting for human accept; agents should not accept XP.',
|
|
254
|
+
task: compactTask(certifiedReview),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const openTask = newest(tasks.filter(task => task.status === 'open'));
|
|
259
|
+
if (openTask) {
|
|
260
|
+
return {
|
|
261
|
+
kind: 'claim_open_task',
|
|
262
|
+
label: `${taskRef(openTask)} ${clip(openTask.title, 90)}`,
|
|
263
|
+
command: `atris task claim ${taskRef(openTask)} --as ${actor}`,
|
|
264
|
+
why: 'Open task is ready to claim.',
|
|
265
|
+
task: compactTask(openTask),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (endgame && endgame.slug) {
|
|
270
|
+
return {
|
|
271
|
+
kind: 'seed_endgame_task',
|
|
272
|
+
label: endgame.slug,
|
|
273
|
+
command: 'atris task next --create-next',
|
|
274
|
+
why: 'No actionable task is open, but TODO has an Endgame horizon.',
|
|
275
|
+
endgame,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (brain && brain.next_move) {
|
|
280
|
+
return {
|
|
281
|
+
kind: 'activate_brain',
|
|
282
|
+
label: clip(brain.next_move, 100),
|
|
283
|
+
command: 'atris brain activate --root . --verify',
|
|
284
|
+
why: 'No task or mission outranks the compiled brain next move.',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
kind: 'ask_for_work',
|
|
290
|
+
label: 'Ask for the next concrete outcome',
|
|
291
|
+
command: 'atris',
|
|
292
|
+
why: 'No task, mission, endgame, or brain next move is available.',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function compactTask(task) {
|
|
297
|
+
if (!task) return null;
|
|
298
|
+
const review = task.review || {};
|
|
299
|
+
return {
|
|
300
|
+
ref: taskRef(task),
|
|
301
|
+
title: task.title || '',
|
|
302
|
+
status: task.status || '',
|
|
303
|
+
tag: task.tag || null,
|
|
304
|
+
claimed_by: task.claimed_by || null,
|
|
305
|
+
agent_review_pass_count: Number(review.agent_review_pass_count || task.metadata && task.metadata.agent_review_pass_count || 0),
|
|
306
|
+
agent_certified: taskIsAgentCertified(task),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function compactMission(mission) {
|
|
311
|
+
if (!mission) return null;
|
|
312
|
+
return {
|
|
313
|
+
id: mission.id,
|
|
314
|
+
owner: mission.owner || null,
|
|
315
|
+
status: mission.status || null,
|
|
316
|
+
objective: mission.objective || '',
|
|
317
|
+
verifier: mission.verifier || null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sortNewest(items) {
|
|
322
|
+
return [...items].sort((a, b) => taskUpdatedAt(b) - taskUpdatedAt(a));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function taskSuggestion({ title, task, why }) {
|
|
326
|
+
return {
|
|
327
|
+
title,
|
|
328
|
+
subject: task && task.title || '',
|
|
329
|
+
ref: taskRef(task) || null,
|
|
330
|
+
why,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function missionSuggestion(mission) {
|
|
335
|
+
return {
|
|
336
|
+
title: 'Run the paused live job',
|
|
337
|
+
subject: mission && mission.objective || '',
|
|
338
|
+
ref: mission && mission.id || null,
|
|
339
|
+
why: 'It is waiting on a check before it can move forward.',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function endgameSuggestion(endgame) {
|
|
344
|
+
return {
|
|
345
|
+
title: 'Turn the goal into a small first task',
|
|
346
|
+
subject: endgame && (endgame.horizon || endgame.slug) || '',
|
|
347
|
+
ref: endgame && endgame.slug || null,
|
|
348
|
+
why: 'The goal exists, but no one has a concrete first move yet.',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function brainSuggestion(brain) {
|
|
353
|
+
return {
|
|
354
|
+
title: 'Use the saved next move',
|
|
355
|
+
subject: brain && brain.next_move || '',
|
|
356
|
+
ref: null,
|
|
357
|
+
why: 'There is no more urgent work item in the queue.',
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function pushSuggestion(list, seen, suggestion) {
|
|
362
|
+
if (!suggestion || !suggestion.subject) return;
|
|
363
|
+
const key = `${suggestion.title}:${suggestion.subject}`;
|
|
364
|
+
if (seen.has(key)) return;
|
|
365
|
+
seen.add(key);
|
|
366
|
+
list.push({
|
|
367
|
+
...suggestion,
|
|
368
|
+
subject: clip(suggestion.subject, 68),
|
|
369
|
+
why: clip(suggestion.why, 86),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function suggestedNextMoves({ actor, tasks, missions, brain, endgame }) {
|
|
374
|
+
const suggestions = [];
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
const actorKey = String(actor || '').toLowerCase();
|
|
377
|
+
|
|
378
|
+
const ownClaimed = sortNewest(tasks.filter(item => (
|
|
379
|
+
item.status === 'claimed'
|
|
380
|
+
&& String(item.claimed_by || '').toLowerCase() === actorKey
|
|
381
|
+
))).slice(0, 2);
|
|
382
|
+
ownClaimed.forEach((task, index) => {
|
|
383
|
+
pushSuggestion(suggestions, seen, taskSuggestion({
|
|
384
|
+
title: index === 0 ? 'Finish current work' : 'Close another open thread',
|
|
385
|
+
task,
|
|
386
|
+
why: 'It is already in progress, so finishing it avoids another loose thread.',
|
|
387
|
+
}));
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const mission = missions.find(missionNeedsTick);
|
|
391
|
+
if (mission) pushSuggestion(suggestions, seen, missionSuggestion(mission));
|
|
392
|
+
|
|
393
|
+
const reviewTask = newest(tasks.filter(taskNeedsAgentReview));
|
|
394
|
+
if (reviewTask) {
|
|
395
|
+
pushSuggestion(suggestions, seen, taskSuggestion({
|
|
396
|
+
title: 'Check a finished change',
|
|
397
|
+
task: reviewTask,
|
|
398
|
+
why: 'It is built, but it needs a second set of eyes before anyone trusts it.',
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const certifiedReview = newest(tasks.filter(task => task.status === 'review' && taskIsAgentCertified(task)));
|
|
403
|
+
if (certifiedReview) {
|
|
404
|
+
pushSuggestion(suggestions, seen, taskSuggestion({
|
|
405
|
+
title: 'Approve or send back checked work',
|
|
406
|
+
task: certifiedReview,
|
|
407
|
+
why: 'It has already been checked and is waiting for a human decision.',
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const openTask = newest(tasks.filter(task => task.status === 'open'));
|
|
412
|
+
if (openTask) {
|
|
413
|
+
pushSuggestion(suggestions, seen, taskSuggestion({
|
|
414
|
+
title: 'Start new work',
|
|
415
|
+
task: openTask,
|
|
416
|
+
why: 'It is unowned and ready for someone to pick up.',
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (endgame && endgame.slug) pushSuggestion(suggestions, seen, endgameSuggestion(endgame));
|
|
421
|
+
if (brain && brain.next_move) pushSuggestion(suggestions, seen, brainSuggestion(brain));
|
|
422
|
+
|
|
423
|
+
return suggestions.slice(0, 3);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function collectLaunchpad(root = process.cwd(), args = []) {
|
|
427
|
+
const actor = readFlag(args, '--as', inferActor());
|
|
428
|
+
const taskState = loadTaskProjection(root);
|
|
429
|
+
const skippedCurrent = newest(taskState.tasks.filter(task => (
|
|
430
|
+
taskIsOwnedByActor(task, actor)
|
|
431
|
+
&& taskIsHumanGatedClaimed(task)
|
|
432
|
+
)));
|
|
433
|
+
const actionableTasks = skippedCurrent
|
|
434
|
+
? taskState.tasks.filter(task => task.id !== skippedCurrent.id)
|
|
435
|
+
: taskState.tasks;
|
|
436
|
+
const missions = latestMissions(root);
|
|
437
|
+
const brain = readBrain(root);
|
|
438
|
+
const endgame = readEndgame(root);
|
|
439
|
+
const counts = summarizeTasks(taskState.tasks);
|
|
440
|
+
counts.missions_active = missions.length;
|
|
441
|
+
counts.missions_need_tick = missions.filter(missionNeedsTick).length;
|
|
442
|
+
counts.hidden_done = taskState.hidden_done_count;
|
|
443
|
+
|
|
444
|
+
const nextAction = chooseNextAction({
|
|
445
|
+
root,
|
|
446
|
+
actor,
|
|
447
|
+
tasks: actionableTasks,
|
|
448
|
+
missions,
|
|
449
|
+
brain,
|
|
450
|
+
endgame,
|
|
451
|
+
});
|
|
452
|
+
if (skippedCurrent) nextAction.skipped_current = compactTask(skippedCurrent);
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
ok: true,
|
|
456
|
+
schema: 'atris.launchpad.v1',
|
|
457
|
+
workspace_root: root,
|
|
458
|
+
workspace: path.basename(root),
|
|
459
|
+
actor,
|
|
460
|
+
counts,
|
|
461
|
+
skipped_current: skippedCurrent ? compactTask(skippedCurrent) : null,
|
|
462
|
+
next_action: nextAction,
|
|
463
|
+
suggestions: suggestedNextMoves({
|
|
464
|
+
actor,
|
|
465
|
+
tasks: actionableTasks,
|
|
466
|
+
missions,
|
|
467
|
+
brain,
|
|
468
|
+
endgame,
|
|
469
|
+
}),
|
|
470
|
+
previews: {
|
|
471
|
+
claimed: taskState.tasks.filter(task => task.status === 'claimed').slice(0, 5).map(compactTask),
|
|
472
|
+
review: taskState.tasks.filter(task => task.status === 'review').slice(0, 5).map(compactTask),
|
|
473
|
+
open: taskState.tasks.filter(task => task.status === 'open').slice(0, 5).map(compactTask),
|
|
474
|
+
missions: missions.slice(0, 5).map(compactMission),
|
|
475
|
+
},
|
|
476
|
+
brain,
|
|
477
|
+
endgame,
|
|
478
|
+
sources: {
|
|
479
|
+
tasks: taskState.projection_path,
|
|
480
|
+
missions: fs.existsSync(path.join(root, '.atris', 'state', 'missions.jsonl')) ? '.atris/state/missions.jsonl' : null,
|
|
481
|
+
brain: brain.status_path,
|
|
482
|
+
todo: fs.existsSync(path.join(root, 'atris', 'TODO.md')) ? 'atris/TODO.md' : null,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function actionTitle(action) {
|
|
488
|
+
const kind = action && action.kind;
|
|
489
|
+
if (kind === 'continue_claimed_task') return 'Work in progress';
|
|
490
|
+
if (kind === 'tick_mission') return 'Live job waiting on a check';
|
|
491
|
+
if (kind === 'review_proof') return 'Built change waiting for a check';
|
|
492
|
+
if (kind === 'human_accept_queue') return 'Checked change waiting for your approval';
|
|
493
|
+
if (kind === 'claim_open_task') return 'New change ready to start';
|
|
494
|
+
if (kind === 'seed_endgame_task') return 'Goal needs a first task';
|
|
495
|
+
if (kind === 'activate_brain') return 'Saved next move is ready';
|
|
496
|
+
if (kind === 'bootstrap') return 'Set up this workspace';
|
|
497
|
+
return 'No clear next move';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function whatHappened(action) {
|
|
501
|
+
const kind = action && action.kind;
|
|
502
|
+
if (kind === 'continue_claimed_task') return 'This change is already yours. Finish it before starting another.';
|
|
503
|
+
if (kind === 'tick_mission') return 'An always-on job is paused until its check runs.';
|
|
504
|
+
if (kind === 'review_proof') return 'This change is built. It needs someone else to check it before it counts as done.';
|
|
505
|
+
if (kind === 'human_accept_queue') return 'This change has been checked. It is waiting for your yes or no.';
|
|
506
|
+
if (kind === 'claim_open_task') return 'This change has not been started yet.';
|
|
507
|
+
if (kind === 'seed_endgame_task') return 'There is a goal, but no small first step exists yet.';
|
|
508
|
+
if (kind === 'activate_brain') return 'There is no urgent work item, so Atris is falling back to the saved next move.';
|
|
509
|
+
if (kind === 'bootstrap') return 'Atris is not set up in this folder yet.';
|
|
510
|
+
return action && action.why || 'No clear next move was found.';
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function afterThis(action) {
|
|
514
|
+
const kind = action && action.kind;
|
|
515
|
+
if (kind === 'continue_claimed_task') return 'When it is ready, send it for a check.';
|
|
516
|
+
if (kind === 'tick_mission') return 'If the check passes, the job can close or move on.';
|
|
517
|
+
if (kind === 'review_proof') return 'If the check passes, you can approve it. If not, it comes back with the fix needed.';
|
|
518
|
+
if (kind === 'human_accept_queue') return 'Approve it if you trust it. Send it back if something feels off.';
|
|
519
|
+
if (kind === 'claim_open_task') return 'After claiming it, Atris will walk the change forward.';
|
|
520
|
+
if (kind === 'seed_endgame_task') return 'After the task exists, an agent can claim and build it.';
|
|
521
|
+
if (kind === 'activate_brain') return 'Atris will reload the saved context and pick from there.';
|
|
522
|
+
if (kind === 'bootstrap') return 'After setup, run atris again.';
|
|
523
|
+
return 'Then rerun atris launchpad.';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function humanSubject(action) {
|
|
527
|
+
if (action && action.task) {
|
|
528
|
+
const ref = action.task.ref ? ` (${action.task.ref})` : '';
|
|
529
|
+
return `${clip(action.task.title || action.label, 68)}${ref}`;
|
|
530
|
+
}
|
|
531
|
+
if (action && action.mission) return clip(action.mission.objective || action.label, 76);
|
|
532
|
+
if (action && action.endgame) return clip(action.endgame.horizon || action.endgame.slug, 76);
|
|
533
|
+
return clip(action && action.label || 'No item selected', 76);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function behindLines(payload) {
|
|
537
|
+
const counts = payload.counts || {};
|
|
538
|
+
const reviewNeedsAgent = counts.review_needs_agent || 0;
|
|
539
|
+
const reviewReadyForHuman = counts.review_certified || 0;
|
|
540
|
+
const claimed = counts.claimed || 0;
|
|
541
|
+
const missions = counts.missions_need_tick || 0;
|
|
542
|
+
const lines = [];
|
|
543
|
+
if (reviewNeedsAgent) lines.push(`${reviewNeedsAgent} built change${reviewNeedsAgent === 1 ? ' still needs' : 's still need'} a check`);
|
|
544
|
+
if (reviewReadyForHuman) lines.push(`${reviewReadyForHuman} checked change${reviewReadyForHuman === 1 ? ' is' : 's are'} waiting for you`);
|
|
545
|
+
if (claimed) lines.push(`${claimed} change${claimed === 1 ? ' is' : 's are'} still in progress`);
|
|
546
|
+
if (missions) lines.push(`${missions} live job${missions === 1 ? ' is' : 's are'} waiting on a check`);
|
|
547
|
+
if (!lines.length) lines.push('Nothing urgent is waiting');
|
|
548
|
+
return lines.slice(0, 4);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function suggestionLines(suggestions) {
|
|
552
|
+
const list = Array.isArray(suggestions) ? suggestions.filter(item => item && item.subject) : [];
|
|
553
|
+
if (!list.length) {
|
|
554
|
+
return [
|
|
555
|
+
' 1. Pick one clear outcome',
|
|
556
|
+
' No waiting work was found.',
|
|
557
|
+
' Say what you want moved forward and Atris can turn it into a task.',
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
return list.flatMap((item, index) => {
|
|
561
|
+
const ref = item.ref && !String(item.subject).includes(String(item.ref)) ? ` (${item.ref})` : '';
|
|
562
|
+
return [
|
|
563
|
+
` ${index + 1}. ${clip(item.title, 48)}`,
|
|
564
|
+
` ${clip(item.subject, 64)}${ref}`,
|
|
565
|
+
` ${clip(item.why, 86)}`,
|
|
566
|
+
];
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function boxLine(text, width = 62) {
|
|
571
|
+
const clipped = clip(text, width - 4);
|
|
572
|
+
return `| ${clipped.padEnd(width - 4)} |`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderLaunchpad(payload) {
|
|
576
|
+
const next = payload.next_action || {};
|
|
577
|
+
const title = actionTitle(next);
|
|
578
|
+
const subject = humanSubject(next);
|
|
579
|
+
const command = next.command || 'atris';
|
|
580
|
+
const happened = whatHappened(next);
|
|
581
|
+
const after = afterThis(next);
|
|
582
|
+
const behind = behindLines(payload);
|
|
583
|
+
const lines = [
|
|
584
|
+
'',
|
|
585
|
+
'+------------------------------------------------------------+',
|
|
586
|
+
boxLine('ATRIS'),
|
|
587
|
+
boxLine(title),
|
|
588
|
+
'+------------------------------------------------------------+',
|
|
589
|
+
'',
|
|
590
|
+
'WHAT HAPPENED',
|
|
591
|
+
` ${subject}`,
|
|
592
|
+
` ${happened}`,
|
|
593
|
+
'',
|
|
594
|
+
'WHAT TO WORK ON NEXT',
|
|
595
|
+
...suggestionLines(payload.suggestions),
|
|
596
|
+
'',
|
|
597
|
+
'RUN THIS',
|
|
598
|
+
` ${command}`,
|
|
599
|
+
'',
|
|
600
|
+
'AFTER THAT',
|
|
601
|
+
` ${after}`,
|
|
602
|
+
'',
|
|
603
|
+
'BEHIND THIS',
|
|
604
|
+
...behind.map(item => ` - ${item}`),
|
|
605
|
+
];
|
|
606
|
+
return `${lines.join('\n')}\n`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function showLaunchpadHelp() {
|
|
610
|
+
console.log('Usage: atris launchpad [--json] [--as <owner>]');
|
|
611
|
+
console.log('');
|
|
612
|
+
console.log('Show one compact command-center card from local brain, task, mission, and proof state.');
|
|
613
|
+
console.log('');
|
|
614
|
+
console.log('Options:');
|
|
615
|
+
console.log(' --json Print machine-readable launchpad state.');
|
|
616
|
+
console.log(' --as <owner> Pick next action for a specific agent/member.');
|
|
617
|
+
console.log(' --help, -h Show this help.');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function launchpadCommand(args = []) {
|
|
621
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
|
|
622
|
+
showLaunchpadHelp();
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
const payload = collectLaunchpad(process.cwd(), args);
|
|
626
|
+
if (hasFlag(args, '--json')) {
|
|
627
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
628
|
+
} else {
|
|
629
|
+
process.stdout.write(renderLaunchpad(payload));
|
|
630
|
+
}
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
module.exports = {
|
|
635
|
+
collectLaunchpad,
|
|
636
|
+
launchpadCommand,
|
|
637
|
+
renderLaunchpad,
|
|
638
|
+
};
|