atris 3.35.0 → 3.36.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 +565 -265
- 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 +3029 -339
- 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 +79 -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,651 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const autoland = require('../lib/autoland');
|
|
9
|
+
const { DENIED_TAGS } = require('../lib/auto-accept-certified');
|
|
10
|
+
const {
|
|
11
|
+
appendBriefRecord,
|
|
12
|
+
buildBriefReview,
|
|
13
|
+
renderBriefReview,
|
|
14
|
+
stampBriefOutcome,
|
|
15
|
+
} = require('../lib/brief-ledger');
|
|
16
|
+
const { renderHtml } = require('../lib/html-render');
|
|
17
|
+
const { nextMoves, recordDecision, seedInboxFromMove } = require('../lib/next-moves');
|
|
18
|
+
const { buildWeekReportData, renderWeekReport } = require('./report');
|
|
19
|
+
|
|
20
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
22
|
+
const NEEDS_HUMAN_TAGS = new Set(['needs-human', 'human-review', 'human', 'manual', 'protected', 'denied']);
|
|
23
|
+
|
|
24
|
+
function readProjection(root) {
|
|
25
|
+
const projectionPath = path.join(root, TASK_PROJECTION_FILE);
|
|
26
|
+
if (!fs.existsSync(projectionPath)) return [];
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
|
|
29
|
+
return Array.isArray(parsed.tasks) ? parsed.tasks : [];
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function timestampMs(value) {
|
|
36
|
+
if (value === null || value === undefined || value === '') return null;
|
|
37
|
+
if (value instanceof Date) {
|
|
38
|
+
const ms = value.getTime();
|
|
39
|
+
return Number.isFinite(ms) ? ms : null;
|
|
40
|
+
}
|
|
41
|
+
const numeric = Number(value);
|
|
42
|
+
if (Number.isFinite(numeric) && String(value).trim() !== '') {
|
|
43
|
+
return numeric < 100000000000 ? numeric * 1000 : numeric;
|
|
44
|
+
}
|
|
45
|
+
const parsed = Date.parse(value);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeNow(now) {
|
|
50
|
+
return timestampMs(now) || Date.now();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inlineText(value, fallback = 'untitled') {
|
|
54
|
+
const text = String(value || '')
|
|
55
|
+
.replace(/[\u2014\u2013]/g, '-')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim();
|
|
58
|
+
return text || fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function outputText(value, fallback = '') {
|
|
62
|
+
return inlineText(value, fallback).toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function finishText(value, max = 150) {
|
|
66
|
+
const clean = inlineText(value, '');
|
|
67
|
+
if (clean.length <= max) return clean;
|
|
68
|
+
const cut = clean.slice(0, max);
|
|
69
|
+
const sentence = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('? '), cut.lastIndexOf('! '));
|
|
70
|
+
if (sentence > max * 0.35) return cut.slice(0, sentence + 1).trim();
|
|
71
|
+
const clause = Math.max(cut.lastIndexOf('; '), cut.lastIndexOf(', '), cut.lastIndexOf(': '));
|
|
72
|
+
if (clause > max * 0.45) return cut.slice(0, clause).trim();
|
|
73
|
+
const space = cut.lastIndexOf(' ');
|
|
74
|
+
return cut.slice(0, space > max * 0.55 ? space : max).trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function operatorTitle(value, max = 140) {
|
|
78
|
+
const raw = inlineText(value, '');
|
|
79
|
+
const plumbing = /`|(?:^|\s)(?:node|npm|git|rg|grep)\s+|\S+\.(?:js|md|json|ts|py|sh)(?::\d+)?|\b[A-Z]{2,5}-\d+\b|\bpr\s*#?\s*\d+\b/i;
|
|
80
|
+
const source = plumbing.test(raw) && raw.includes(':') ? raw.split(':')[0] : raw;
|
|
81
|
+
const scrubbed = source
|
|
82
|
+
.replace(/`[^`]*`/g, ' ')
|
|
83
|
+
.replace(/\b(?:done|check|verify|proof):\s+.*$/i, ' ')
|
|
84
|
+
.replace(/\b(?:node|npm|git|rg|grep)\s+[^\.;,]+/gi, ' ')
|
|
85
|
+
.replace(/\S+\.(?:js|md|json|ts|py|sh)(?::\d+)?/gi, ' ')
|
|
86
|
+
.replace(/\b[A-Z]{2,5}-\d+\b/g, ' ')
|
|
87
|
+
.replace(/\bpr\s*#?\s*\d+\b/gi, ' ')
|
|
88
|
+
.replace(/\s+/g, ' ')
|
|
89
|
+
.replace(/\([^a-z0-9]*\)/gi, ' ')
|
|
90
|
+
.replace(/,\s*,+/g, ',')
|
|
91
|
+
.replace(/\s+([,.;:])/g, '$1')
|
|
92
|
+
.replace(/([:(])\s*([,.;:)]|$)/g, '')
|
|
93
|
+
.replace(/\s+\)/g, ')')
|
|
94
|
+
.trim();
|
|
95
|
+
return finishText(scrubbed || value, max);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function taskId(task) {
|
|
99
|
+
return inlineText(task.display_id || task.legacy_ref || task.task_ref || task.id, 'task');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function taskTime(task, keys) {
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
const value = key.split('.').reduce((obj, part) => (obj && obj[part] !== undefined ? obj[part] : undefined), task);
|
|
105
|
+
const ms = timestampMs(value);
|
|
106
|
+
if (ms !== null) return ms;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function taskTags(task) {
|
|
112
|
+
const values = [
|
|
113
|
+
task.tag,
|
|
114
|
+
task.metadata && task.metadata.tag,
|
|
115
|
+
...(Array.isArray(task.tags) ? task.tags : []),
|
|
116
|
+
...(Array.isArray(task.metadata && task.metadata.tags) ? task.metadata.tags : []),
|
|
117
|
+
];
|
|
118
|
+
return values
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.flatMap((tag) => String(tag).toLowerCase().split(/[^a-z0-9-]+/))
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function deniedTagForTask(task) {
|
|
125
|
+
const tags = taskTags(task);
|
|
126
|
+
for (const tag of tags) {
|
|
127
|
+
for (const denied of DENIED_TAGS) {
|
|
128
|
+
if (tag === denied || tag.replace(/s$/, '') === denied) return denied;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function needsHumanTag(task) {
|
|
135
|
+
return taskTags(task).find((tag) => NEEDS_HUMAN_TAGS.has(tag)) || null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractPointer(text) {
|
|
139
|
+
const value = inlineText(text, '');
|
|
140
|
+
if (!value) return '';
|
|
141
|
+
const url = value.match(/https?:\/\/[^\s<>"')]+/i);
|
|
142
|
+
if (url) return url[0].replace(/[.,;]+$/, '');
|
|
143
|
+
const pathMatch = value.match(/(?:^|\s)((?:\.atris|atris)\/[^\s,;:)]+)/i);
|
|
144
|
+
if (pathMatch) return pathMatch[1].replace(/[.,;]+$/, '');
|
|
145
|
+
const pr = value.match(/\bpr\s*#?\s*\d+\b/i);
|
|
146
|
+
if (pr) return pr[0].replace(/\s+/g, ' ');
|
|
147
|
+
return /\b(pass(?:ed)?|verified|green|merged|ok)\b/i.test(value) ? 'proof on file' : '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function proofPointer(task) {
|
|
151
|
+
const metadata = task.metadata || {};
|
|
152
|
+
const review = task.review || {};
|
|
153
|
+
const landing = review.landing || {};
|
|
154
|
+
const candidates = [
|
|
155
|
+
metadata.receipt_path,
|
|
156
|
+
metadata.receipt,
|
|
157
|
+
metadata.receipt_file,
|
|
158
|
+
metadata.landing_receipt,
|
|
159
|
+
metadata.landing_url,
|
|
160
|
+
metadata.landing_pr,
|
|
161
|
+
landing.receipt,
|
|
162
|
+
landing.proof,
|
|
163
|
+
metadata.latest_agent_proof,
|
|
164
|
+
metadata.proof,
|
|
165
|
+
metadata.verify,
|
|
166
|
+
];
|
|
167
|
+
for (const candidate of candidates) {
|
|
168
|
+
const pointer = extractPointer(candidate);
|
|
169
|
+
if (pointer) return pointer;
|
|
170
|
+
}
|
|
171
|
+
return 'task projection';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatAge(start, nowMs) {
|
|
175
|
+
const ms = timestampMs(start);
|
|
176
|
+
if (ms === null) return 'age unknown';
|
|
177
|
+
const delta = Math.max(0, nowMs - ms);
|
|
178
|
+
const minutes = Math.floor(delta / 60000);
|
|
179
|
+
if (minutes < 1) return 'just now';
|
|
180
|
+
if (minutes < 60) return `${minutes}m`;
|
|
181
|
+
const hours = Math.floor(minutes / 60);
|
|
182
|
+
if (hours < 48) return `${hours}h`;
|
|
183
|
+
return `${Math.floor(hours / 24)}d`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function waitingReason(task) {
|
|
187
|
+
const approval = String(task.review?.approval_status || task.metadata?.approval_status || '').toLowerCase();
|
|
188
|
+
if (approval && approval !== 'pending') return `review lane is ${approval}`;
|
|
189
|
+
const denied = deniedTagForTask(task);
|
|
190
|
+
if (denied) return `protected ${denied} lane`;
|
|
191
|
+
if (needsHumanTag(task)) return 'needs human lane';
|
|
192
|
+
return 'review lane';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isWaitingTask(task) {
|
|
196
|
+
const status = String(task.status || '').toLowerCase();
|
|
197
|
+
if (status === 'done' || status === 'failed' || status === 'cancelled') return false;
|
|
198
|
+
return status === 'review' || Boolean(deniedTagForTask(task) || needsHumanTag(task));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildLanded(tasks, cutoffMs, nowMs) {
|
|
202
|
+
return tasks
|
|
203
|
+
.filter((task) => String(task.status || '').toLowerCase() === 'done')
|
|
204
|
+
.map((task) => {
|
|
205
|
+
const doneMs = taskTime(task, ['done_at', 'metadata.accepted_at', 'updated_at', 'created_at']);
|
|
206
|
+
return { task, doneMs };
|
|
207
|
+
})
|
|
208
|
+
.filter(({ doneMs }) => doneMs !== null && doneMs >= cutoffMs)
|
|
209
|
+
.sort((a, b) => b.doneMs - a.doneMs)
|
|
210
|
+
.map(({ task, doneMs }) => ({
|
|
211
|
+
id: taskId(task),
|
|
212
|
+
title: inlineText(task.title, 'done task'),
|
|
213
|
+
proof: proofPointer(task),
|
|
214
|
+
age: formatAge(doneMs, nowMs),
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildWaiting(tasks, nowMs) {
|
|
219
|
+
return tasks
|
|
220
|
+
.filter(isWaitingTask)
|
|
221
|
+
.map((task) => {
|
|
222
|
+
const startMs = taskTime(task, ['metadata.agent_certified_at', 'updated_at', 'created_at']);
|
|
223
|
+
return {
|
|
224
|
+
id: taskId(task),
|
|
225
|
+
title: inlineText(task.title, 'review task'),
|
|
226
|
+
why: waitingReason(task),
|
|
227
|
+
age: formatAge(startMs, nowMs),
|
|
228
|
+
_sort: startMs || 0,
|
|
229
|
+
};
|
|
230
|
+
})
|
|
231
|
+
.sort((a, b) => a._sort - b._sort)
|
|
232
|
+
.map(({ _sort, ...item }) => item);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildMoves(root) {
|
|
236
|
+
return nextMoves(root, 3).map((move) => ({
|
|
237
|
+
id: move.id,
|
|
238
|
+
title: inlineText(move.title, 'next move'),
|
|
239
|
+
why: inlineText(move.why || move.source || 'workspace signal', 'workspace signal'),
|
|
240
|
+
source: move.source || null,
|
|
241
|
+
owner: move.owner || move.member || null,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildWeekLine(root, nowMs) {
|
|
246
|
+
const data = buildWeekReportData(root, { days: 7, now: nowMs });
|
|
247
|
+
return renderWeekReport(data).split(/\r?\n/)[0] || 'week in review: 0 landed, 0 completions, 0 xp';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildBriefData(root = process.cwd(), { days = 1, now = Date.now() } = {}) {
|
|
251
|
+
const windowDays = Number.isFinite(Number(days)) && Number(days) > 0 ? Math.floor(Number(days)) : 1;
|
|
252
|
+
const nowMs = normalizeNow(now);
|
|
253
|
+
const cutoffMs = nowMs - windowDays * DAY_MS;
|
|
254
|
+
const tasks = readProjection(root);
|
|
255
|
+
return {
|
|
256
|
+
schema: 'atris.brief.v1',
|
|
257
|
+
days: windowDays,
|
|
258
|
+
landed: buildLanded(tasks, cutoffMs, nowMs),
|
|
259
|
+
waiting: buildWaiting(tasks, nowMs),
|
|
260
|
+
moves: buildMoves(root),
|
|
261
|
+
week: buildWeekLine(root, nowMs),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function plural(count, word, pluralWord = `${word}s`) {
|
|
266
|
+
return `${count} ${count === 1 ? word : pluralWord}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderBrief(data) {
|
|
270
|
+
const landed = Array.isArray(data?.landed) ? data.landed : [];
|
|
271
|
+
const waiting = Array.isArray(data?.waiting) ? data.waiting : [];
|
|
272
|
+
const moves = Array.isArray(data?.moves) ? data.moves : [];
|
|
273
|
+
const days = Number(data?.days) || 1;
|
|
274
|
+
const window = days === 1 ? 'last 24h' : `last ${days} days`;
|
|
275
|
+
const lines = [];
|
|
276
|
+
|
|
277
|
+
lines.push(outputText(`atris brief: ${landed.length} landed, ${waiting.length} waiting, ${moves.length} next moves | size: ${window} | know: local atris state`));
|
|
278
|
+
lines.push('');
|
|
279
|
+
|
|
280
|
+
lines.push(outputText(`landed: ${landed.length ? 'finished work' : 'nothing finished yet'} | size: ${plural(landed.length, 'task')} | know: task projection`));
|
|
281
|
+
for (const item of landed.slice(0, 5)) {
|
|
282
|
+
lines.push(outputText(`- ${operatorTitle(item.title)} | size: 1 task | know: ${item.proof || 'proof on file'} (${item.age})`));
|
|
283
|
+
}
|
|
284
|
+
const held = Math.max(0, landed.length - 5);
|
|
285
|
+
if (held > 0) lines.push(outputText(`- held: ${held} more landed | size: ${plural(held, 'task')} | know: task projection`));
|
|
286
|
+
if (!landed.length) lines.push(outputText('- clear: no done tasks landed in this window | size: 0 tasks | know: task projection'));
|
|
287
|
+
lines.push('');
|
|
288
|
+
|
|
289
|
+
lines.push(outputText(`waiting on you: ${waiting.length ? 'review work needs a decision' : 'nothing waiting'} | size: ${plural(waiting.length, 'task')} | know: review lane`));
|
|
290
|
+
if (waiting.length) {
|
|
291
|
+
waiting.forEach((item, index) => {
|
|
292
|
+
lines.push(outputText(`${index + 1}. ${operatorTitle(item.title)} | size: 1 task | know: ${item.why}, ${item.age} old`));
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
lines.push(outputText('1. clear | size: 0 tasks | know: review lane'));
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
298
|
+
|
|
299
|
+
lines.push(outputText(`next moves: ${moves.length ? 'ranked work is ready' : 'nothing ranked'} | size: ${plural(moves.length, 'move')} | know: next-moves scan`));
|
|
300
|
+
if (moves.length) {
|
|
301
|
+
moves.forEach((move, index) => {
|
|
302
|
+
const owner = move.owner ? `; best fit ${move.owner}` : '';
|
|
303
|
+
lines.push(outputText(`${index + 1}. ${operatorTitle(move.title)} | size: 1 move | know: ${move.why}${owner}`));
|
|
304
|
+
});
|
|
305
|
+
} else {
|
|
306
|
+
lines.push(outputText('1. clear | size: 0 moves | know: next-moves scan'));
|
|
307
|
+
}
|
|
308
|
+
lines.push('');
|
|
309
|
+
|
|
310
|
+
lines.push(outputText(`${data?.week || 'week in review: 0 landed, 0 completions, 0 xp'} | size: 7 days | know: report data`));
|
|
311
|
+
return lines.join('\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function panelRows(items, emptyTitle, emptySub, valueLabel) {
|
|
315
|
+
if (!items.length) {
|
|
316
|
+
return [{ title: emptyTitle, sub: emptySub, value: '0', valueSub: valueLabel, sev: 2 }];
|
|
317
|
+
}
|
|
318
|
+
return items.map((item, index) => ({
|
|
319
|
+
title: `${index + 1}. ${item.title}`,
|
|
320
|
+
sub: item.why ? `${item.why}, ${item.age || ''}` : item.proof || item.source || '',
|
|
321
|
+
value: '1',
|
|
322
|
+
valueSub: valueLabel,
|
|
323
|
+
sev: index % 3,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderBriefHtml(data) {
|
|
328
|
+
const landed = Array.isArray(data?.landed) ? data.landed : [];
|
|
329
|
+
const waiting = Array.isArray(data?.waiting) ? data.waiting : [];
|
|
330
|
+
const moves = Array.isArray(data?.moves) ? data.moves : [];
|
|
331
|
+
const spec = {
|
|
332
|
+
theme: 'atris',
|
|
333
|
+
brand: { name: 'atris', accent: ' brief' },
|
|
334
|
+
blocks: [
|
|
335
|
+
{
|
|
336
|
+
type: 'title',
|
|
337
|
+
headline: 'atris brief',
|
|
338
|
+
sub: `${landed.length} landed, ${waiting.length} waiting, ${moves.length} next moves`,
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
type: 'bignumber',
|
|
342
|
+
number: String(landed.length),
|
|
343
|
+
label: 'landed in the current window',
|
|
344
|
+
sub: 'known from task projection and proof pointers',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
type: 'panel',
|
|
348
|
+
heading: 'waiting on you',
|
|
349
|
+
sub: waiting.length ? 'approve or send back' : 'clear',
|
|
350
|
+
panel: {
|
|
351
|
+
header: { title: 'waiting on you', meta: 'review lane' },
|
|
352
|
+
rows: panelRows(waiting, 'nothing waiting', 'review lane is clear', 'task'),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
type: 'panel',
|
|
357
|
+
heading: 'next moves',
|
|
358
|
+
sub: moves.length ? 'ranked from current workspace state' : 'nothing ranked',
|
|
359
|
+
panel: {
|
|
360
|
+
header: { title: 'next moves', meta: 'top 3' },
|
|
361
|
+
rows: panelRows(moves, 'nothing ranked', 'next-moves scan is clear', 'move'),
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
type: 'close',
|
|
366
|
+
tagline: `${data?.week || 'week in review: 0 landed, 0 completions, 0 xp'} | size: 7 days | know: report data`,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
return renderHtml(spec, { title: 'atris brief' });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function readFlagValue(args, name, fallback = null) {
|
|
374
|
+
const prefix = `${name}=`;
|
|
375
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
376
|
+
const arg = args[index];
|
|
377
|
+
if (arg === name) return args[index + 1] || fallback;
|
|
378
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length) || fallback;
|
|
379
|
+
}
|
|
380
|
+
return fallback;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseArgs(args = []) {
|
|
384
|
+
return {
|
|
385
|
+
days: readFlagValue(args, '--days', 1),
|
|
386
|
+
out: readFlagValue(args, '--out', null),
|
|
387
|
+
help: args.includes('--help') || args.includes('-h') || args[0] === 'help',
|
|
388
|
+
html: args.includes('--html'),
|
|
389
|
+
json: args.includes('--json'),
|
|
390
|
+
send: args.includes('--send'),
|
|
391
|
+
noInput: args.includes('--no-input'),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function showHelp() {
|
|
396
|
+
console.log('Usage: atris brief [--json] [--html [--out FILE]] [--send] [--no-input] [--days N]');
|
|
397
|
+
console.log(' atris brief log --engine <engine> --prompt-file <file> [--json|--raw]');
|
|
398
|
+
console.log(' atris brief outcome <brief_id> --result pass|fail|partial --note <text> [--json|--raw]');
|
|
399
|
+
console.log(' atris brief review [--lessons] [--json] [--limit N]');
|
|
400
|
+
console.log('Shows what landed, what waits on you, and what the loop should do next.');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isBriefLedgerSubcommand(value) {
|
|
404
|
+
return ['log', 'outcome', 'review'].includes(String(value || '').trim());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function flagPresent(args, name) {
|
|
408
|
+
return args.includes(name);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function ledgerFlagValue(args, name, fallback = '') {
|
|
412
|
+
const prefix = `${name}=`;
|
|
413
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
414
|
+
const arg = String(args[index]);
|
|
415
|
+
if (arg === name) return args[index + 1] || fallback;
|
|
416
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length) || fallback;
|
|
417
|
+
}
|
|
418
|
+
return fallback;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function positionalArgs(args) {
|
|
422
|
+
const out = [];
|
|
423
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
424
|
+
const arg = String(args[index]);
|
|
425
|
+
if (arg.startsWith('--')) {
|
|
426
|
+
if (!arg.includes('=') && args[index + 1] && !String(args[index + 1]).startsWith('--')) index += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
out.push(arg);
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function runBriefLog(args, root) {
|
|
435
|
+
const json = flagPresent(args, '--json');
|
|
436
|
+
const raw = flagPresent(args, '--raw');
|
|
437
|
+
const engine = ledgerFlagValue(args, '--engine');
|
|
438
|
+
const promptFile = ledgerFlagValue(args, '--prompt-file');
|
|
439
|
+
if (!engine || !promptFile) {
|
|
440
|
+
const message = 'usage: atris brief log --engine <engine> --prompt-file <file>';
|
|
441
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
442
|
+
else console.error(message);
|
|
443
|
+
return 2;
|
|
444
|
+
}
|
|
445
|
+
let promptText;
|
|
446
|
+
try {
|
|
447
|
+
promptText = fs.readFileSync(path.resolve(root, promptFile), 'utf8');
|
|
448
|
+
} catch (err) {
|
|
449
|
+
const message = `brief log: could not read prompt file: ${err.message}`;
|
|
450
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
451
|
+
else console.error(message);
|
|
452
|
+
return 2;
|
|
453
|
+
}
|
|
454
|
+
const worktree = ledgerFlagValue(args, '--worktree', root);
|
|
455
|
+
const record = appendBriefRecord(root, {
|
|
456
|
+
author: ledgerFlagValue(args, '--author', 'external'),
|
|
457
|
+
engine,
|
|
458
|
+
task_id: ledgerFlagValue(args, '--task-id'),
|
|
459
|
+
mission_id: ledgerFlagValue(args, '--mission-id'),
|
|
460
|
+
prompt_text: promptText,
|
|
461
|
+
context: {
|
|
462
|
+
worktree: path.resolve(root, worktree),
|
|
463
|
+
base_ref: ledgerFlagValue(args, '--base-ref'),
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
if (json) console.log(JSON.stringify({ ok: true, record }, null, 2));
|
|
467
|
+
else if (raw) console.log(record.brief_id);
|
|
468
|
+
else console.log(`brief logged for ${record.engine}; outcome can be attached after the flight`);
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function runBriefOutcome(args, root) {
|
|
473
|
+
const json = flagPresent(args, '--json');
|
|
474
|
+
const raw = flagPresent(args, '--raw');
|
|
475
|
+
const id = positionalArgs(args)[0] || '';
|
|
476
|
+
const result = ledgerFlagValue(args, '--result');
|
|
477
|
+
const note = ledgerFlagValue(args, '--note');
|
|
478
|
+
if (!id || !result) {
|
|
479
|
+
const message = 'usage: atris brief outcome <brief_id> --result pass|fail|partial --note <text>';
|
|
480
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
481
|
+
else console.error(message);
|
|
482
|
+
return 2;
|
|
483
|
+
}
|
|
484
|
+
let stamped;
|
|
485
|
+
try {
|
|
486
|
+
stamped = stampBriefOutcome(root, id, { result, note });
|
|
487
|
+
} catch (err) {
|
|
488
|
+
if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
|
|
489
|
+
else console.error(`brief outcome: ${err.message}`);
|
|
490
|
+
return 2;
|
|
491
|
+
}
|
|
492
|
+
if (!stamped.ok) {
|
|
493
|
+
if (json) console.log(JSON.stringify(stamped, null, 2));
|
|
494
|
+
else console.error('brief outcome: brief not found');
|
|
495
|
+
return 1;
|
|
496
|
+
}
|
|
497
|
+
if (json) console.log(JSON.stringify({ ok: true, record: stamped.record }, null, 2));
|
|
498
|
+
else if (raw) console.log(`${stamped.record.brief_id} ${stamped.record.outcome.result}`);
|
|
499
|
+
else console.log(`brief outcome recorded: ${stamped.record.outcome.result}`);
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function runBriefReview(args, root) {
|
|
504
|
+
const json = flagPresent(args, '--json');
|
|
505
|
+
const lessons = flagPresent(args, '--lessons');
|
|
506
|
+
const limit = Number(ledgerFlagValue(args, '--limit', '20')) || 20;
|
|
507
|
+
const review = buildBriefReview(root, { limit, lessons });
|
|
508
|
+
if (json) console.log(JSON.stringify(review, null, 2));
|
|
509
|
+
else console.log(renderBriefReview(review, { lessons }));
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function runBriefLedgerCommand(args, root) {
|
|
514
|
+
const subcommand = String(args[0] || '').trim();
|
|
515
|
+
const rest = args.slice(1);
|
|
516
|
+
if (subcommand === 'log') return runBriefLog(rest, root);
|
|
517
|
+
if (subcommand === 'outcome') return runBriefOutcome(rest, root);
|
|
518
|
+
if (subcommand === 'review') return runBriefReview(rest, root);
|
|
519
|
+
return 2;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function shouldPromptBrief({ flags, stdin = process.stdin, stdout = process.stdout, data }) {
|
|
523
|
+
if (flags?.noInput) return false;
|
|
524
|
+
if (!stdin?.isTTY || !stdout?.isTTY) return false;
|
|
525
|
+
return Boolean((data?.waiting || []).length || (data?.moves || []).length);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function runTaskAccept(root, id) {
|
|
529
|
+
const bin = path.join(root, 'bin', 'atris.js');
|
|
530
|
+
const argv = fs.existsSync(bin)
|
|
531
|
+
? [process.execPath, [bin, 'task', 'accept', id]]
|
|
532
|
+
: ['atris', ['task', 'accept', id]];
|
|
533
|
+
const result = spawnSync(argv[0], argv[1], { cwd: root, encoding: 'utf8', timeout: 30000 });
|
|
534
|
+
return {
|
|
535
|
+
ok: result.status === 0,
|
|
536
|
+
output: inlineText(result.stdout || result.stderr || ''),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function approveMove(root, move, stamp = new Date().toISOString()) {
|
|
541
|
+
recordDecision(root, move, 'approve', stamp);
|
|
542
|
+
return seedInboxFromMove(root, move);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parsePromptAnswer(answer) {
|
|
546
|
+
const text = String(answer || '').trim().toLowerCase();
|
|
547
|
+
if (!text) return { action: 'skip' };
|
|
548
|
+
const accept = text.match(/^a\s+?(\d+)$/);
|
|
549
|
+
if (accept) return { action: 'accept', index: Number(accept[1]) - 1 };
|
|
550
|
+
const move = text.match(/^m\s+?(\d+)$/);
|
|
551
|
+
if (move) return { action: 'move', index: Number(move[1]) - 1 };
|
|
552
|
+
return { action: 'invalid' };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function handleBriefAnswer(root, data, answer, deps = {}) {
|
|
556
|
+
const parsed = parsePromptAnswer(answer);
|
|
557
|
+
const acceptTask = deps.acceptTask || ((id) => runTaskAccept(root, id));
|
|
558
|
+
const moveApproval = deps.approveMove || ((move) => approveMove(root, move, deps.stamp));
|
|
559
|
+
if (parsed.action === 'skip') return { ok: true, skipped: true, message: 'skipped' };
|
|
560
|
+
if (parsed.action === 'accept') {
|
|
561
|
+
const item = (data.waiting || [])[parsed.index];
|
|
562
|
+
if (!item) return { ok: false, message: 'no waiting item for that number' };
|
|
563
|
+
const result = acceptTask(item.id);
|
|
564
|
+
return { ok: result.ok, message: result.output || (result.ok ? `accepted ${item.id}` : `could not accept ${item.id}`) };
|
|
565
|
+
}
|
|
566
|
+
if (parsed.action === 'move') {
|
|
567
|
+
const move = (data.moves || [])[parsed.index];
|
|
568
|
+
if (!move) return { ok: false, message: 'no move for that number' };
|
|
569
|
+
const seeded = moveApproval(move);
|
|
570
|
+
const note = seeded && seeded.alreadyPresent ? 'already in the inbox' : 'seeded into the loop';
|
|
571
|
+
return { ok: true, message: `approved move ${parsed.index + 1}: ${note}` };
|
|
572
|
+
}
|
|
573
|
+
return { ok: false, message: 'no matching brief action' };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function promptBrief(root, data, io = {}) {
|
|
577
|
+
const input = io.stdin || process.stdin;
|
|
578
|
+
const output = io.stdout || process.stdout;
|
|
579
|
+
const answer = await new Promise((resolve) => {
|
|
580
|
+
const rl = readline.createInterface({ input, output });
|
|
581
|
+
rl.question('a N accept waiting, m N approve move, or enter to skip: ', (value) => {
|
|
582
|
+
rl.close();
|
|
583
|
+
resolve(value);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
const result = handleBriefAnswer(root, data, answer);
|
|
587
|
+
if (result.message && !result.skipped) console.log(result.message);
|
|
588
|
+
return result.ok ? 0 : 1;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function sendBrief(root, text) {
|
|
592
|
+
const policy = autoland.readPolicy(root) || {};
|
|
593
|
+
const to = String(policy.imessage_to || '').trim();
|
|
594
|
+
if (!to) {
|
|
595
|
+
console.error('no iMessage recipient configured. set one with: atris autoland on --to <your number>');
|
|
596
|
+
return 1;
|
|
597
|
+
}
|
|
598
|
+
const sent = autoland.sendImessage(root, to, text);
|
|
599
|
+
if (!sent.ok) {
|
|
600
|
+
console.error(sent.output || `could not send brief to ${to}`);
|
|
601
|
+
return 1;
|
|
602
|
+
}
|
|
603
|
+
console.log(`sent brief to ${to}`);
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function briefCommand(args = [], root = process.cwd(), io = {}) {
|
|
608
|
+
const argv = Array.isArray(args) ? args : Array.from(arguments);
|
|
609
|
+
if (isBriefLedgerSubcommand(argv[0])) return runBriefLedgerCommand(argv, root);
|
|
610
|
+
const flags = parseArgs(argv);
|
|
611
|
+
if (flags.help) {
|
|
612
|
+
showHelp();
|
|
613
|
+
return 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const data = buildBriefData(root, { days: flags.days });
|
|
617
|
+
if (flags.json) {
|
|
618
|
+
console.log(JSON.stringify(data, null, 2));
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
if (flags.html) {
|
|
622
|
+
const html = renderBriefHtml(data);
|
|
623
|
+
if (flags.out) {
|
|
624
|
+
const outPath = path.resolve(root, flags.out);
|
|
625
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
626
|
+
fs.writeFileSync(outPath, html, 'utf8');
|
|
627
|
+
console.log(`wrote ${outPath}`);
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
console.log(html);
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const text = renderBrief(data);
|
|
635
|
+
if (flags.send) return sendBrief(root, text);
|
|
636
|
+
console.log(text);
|
|
637
|
+
if (!shouldPromptBrief({ flags, stdin: io.stdin || process.stdin, stdout: io.stdout || process.stdout, data })) return 0;
|
|
638
|
+
return promptBrief(root, data, io);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
module.exports = {
|
|
642
|
+
buildBriefData,
|
|
643
|
+
briefCommand,
|
|
644
|
+
handleBriefAnswer,
|
|
645
|
+
parsePromptAnswer,
|
|
646
|
+
renderBrief,
|
|
647
|
+
renderBriefHtml,
|
|
648
|
+
run: briefCommand,
|
|
649
|
+
runBriefLedgerCommand,
|
|
650
|
+
shouldPromptBrief,
|
|
651
|
+
};
|