atris 3.34.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- 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 +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- 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/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- 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/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- 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/radar.js +259 -14
- 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/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -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 +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- 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 +2431 -38
- 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/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -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/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -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 +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- 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
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const { collectBoard } = require('./land');
|
|
8
|
+
const { listWorktrees } = require('./worktree');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_INTERVAL_MS = 3000;
|
|
11
|
+
const DEFAULT_TAIL_LIMIT = 200;
|
|
12
|
+
const MAX_SUMMARY = 180;
|
|
13
|
+
const ACTIVE_MISSION_STATES = new Set(['planning', 'active', 'running', 'ready']);
|
|
14
|
+
const ACTIVE_TASK_STATES = new Set(['claimed', 'do', 'doing', 'in_progress', 'review']);
|
|
15
|
+
|
|
16
|
+
function defaultDeps() {
|
|
17
|
+
return {
|
|
18
|
+
existsSync: fs.existsSync,
|
|
19
|
+
readFileSync: fs.readFileSync,
|
|
20
|
+
readdirSync: fs.readdirSync,
|
|
21
|
+
statSync: fs.statSync,
|
|
22
|
+
watch: fs.watch,
|
|
23
|
+
spawnSync,
|
|
24
|
+
now: () => Date.now(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasFlag(args, name) {
|
|
29
|
+
return args.includes(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readFlag(args, name, fallback = '') {
|
|
33
|
+
const prefix = `${name}=`;
|
|
34
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
35
|
+
const arg = String(args[i] || '');
|
|
36
|
+
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return String(args[i + 1]);
|
|
37
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
38
|
+
}
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function timestampMs(value) {
|
|
43
|
+
if (value == null || value === '') return 0;
|
|
44
|
+
if (typeof value === 'number') {
|
|
45
|
+
if (!Number.isFinite(value)) return 0;
|
|
46
|
+
return value > 1000000000000 ? value : value * 1000;
|
|
47
|
+
}
|
|
48
|
+
const asNumber = Number(value);
|
|
49
|
+
if (Number.isFinite(asNumber) && String(value).trim() !== '') return timestampMs(asNumber);
|
|
50
|
+
const parsed = Date.parse(String(value));
|
|
51
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isoFrom(value) {
|
|
55
|
+
const ms = timestampMs(value);
|
|
56
|
+
return ms ? new Date(ms).toISOString() : '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeJson(text, fallback = null) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(String(text));
|
|
62
|
+
} catch {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readJson(file, deps, fallback = null) {
|
|
68
|
+
try {
|
|
69
|
+
if (!deps.existsSync(file)) return fallback;
|
|
70
|
+
return safeJson(deps.readFileSync(file, 'utf8'), fallback);
|
|
71
|
+
} catch {
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readJsonl(file, deps, { tail = DEFAULT_TAIL_LIMIT } = {}) {
|
|
77
|
+
try {
|
|
78
|
+
if (!deps.existsSync(file)) return [];
|
|
79
|
+
const lines = String(deps.readFileSync(file, 'utf8')).split(/\r?\n/).filter(Boolean);
|
|
80
|
+
return lines.slice(-tail).map((line) => safeJson(line)).filter(Boolean);
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function listDir(dir, deps) {
|
|
87
|
+
try {
|
|
88
|
+
if (!deps.existsSync(dir)) return [];
|
|
89
|
+
return deps.readdirSync(dir);
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isDirectory(file, deps) {
|
|
96
|
+
try {
|
|
97
|
+
return deps.existsSync(file) && deps.statSync(file).isDirectory();
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runGit(root, args, deps) {
|
|
104
|
+
try {
|
|
105
|
+
const result = deps.spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
106
|
+
return result && typeof result.status === 'number' ? result : { status: 1, stdout: '', stderr: '' };
|
|
107
|
+
} catch {
|
|
108
|
+
return { status: 1, stdout: '', stderr: '' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function repoRoot(cwd = process.cwd(), deps = defaultDeps()) {
|
|
113
|
+
const result = runGit(cwd, ['rev-parse', '--show-toplevel'], deps);
|
|
114
|
+
return result.status === 0 && result.stdout ? result.stdout.trim() : path.resolve(cwd);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function firstText(...values) {
|
|
118
|
+
for (const value of values) {
|
|
119
|
+
const text = String(value == null ? '' : value)
|
|
120
|
+
.split(/\r?\n/)
|
|
121
|
+
.map((line) => line.trim().replace(/^[-*]\s+/, ''))
|
|
122
|
+
.find(Boolean);
|
|
123
|
+
if (text) return text;
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function clip(value, limit = MAX_SUMMARY) {
|
|
129
|
+
const text = String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
|
130
|
+
return text.length > limit ? `${text.slice(0, limit - 3)}...` : text;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sentence(value, fallback = 'updated the work') {
|
|
134
|
+
let text = clip(firstText(value) || fallback);
|
|
135
|
+
text = text.replace(/^(summary|result|receipt|final answer|changed|proof):\s*/i, '');
|
|
136
|
+
if (!/[.!?]$/.test(text)) text += '.';
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function taskTitle(task) {
|
|
141
|
+
return clip(task && (task.title || task.objective || task.result), 90) || 'the task';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function payloadText(payload, ...keys) {
|
|
145
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
146
|
+
for (const key of keys) {
|
|
147
|
+
if (payload[key] != null && payload[key] !== '') return payload[key];
|
|
148
|
+
}
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function taskEventSummary(task, event = {}) {
|
|
153
|
+
const payload = event.payload || {};
|
|
154
|
+
const title = taskTitle(task);
|
|
155
|
+
const type = String(event.event_type || event.type || 'updated');
|
|
156
|
+
if (type === 'claimed') return `started ${title}`;
|
|
157
|
+
if (type === 'proof_ready' || type === 'ready') return `finished ${title}; proof is ready`;
|
|
158
|
+
if (type === 'completed' || type === 'done') return `finished ${title}`;
|
|
159
|
+
if (type === 'accepted' || type === 'auto_accepted') return `accepted ${title}`;
|
|
160
|
+
if (type === 'revision_requested') return `bounced ${title}: ${payloadText(payload, 'lesson', 'summary', 'reason') || 'needs another pass'}`;
|
|
161
|
+
if (type === 'reviewed') {
|
|
162
|
+
const reward = Number(payload.reward);
|
|
163
|
+
if (Number.isFinite(reward) && reward < 0) return `bounced ${title}: ${payloadText(payload, 'lesson', 'summary') || 'review failed'}`;
|
|
164
|
+
return `validated ${title}`;
|
|
165
|
+
}
|
|
166
|
+
if (type === 'message' || type === 'comment') return `left a note on ${title}`;
|
|
167
|
+
return `updated ${title}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function taskStateSummary(task) {
|
|
171
|
+
const title = taskTitle(task);
|
|
172
|
+
const status = String(task && task.status || 'updated');
|
|
173
|
+
if (status === 'claimed' || status === 'do' || status === 'doing') return `started ${title}`;
|
|
174
|
+
if (status === 'review') return `sent ${title} to review`;
|
|
175
|
+
if (status === 'done') return `finished ${title}`;
|
|
176
|
+
if (status === 'blocked' || status === 'failed') return `got blocked on ${title}`;
|
|
177
|
+
return `updated ${title}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function taskEpisodeSummary(row) {
|
|
181
|
+
const state = row && row.state || {};
|
|
182
|
+
const action = row && row.action || {};
|
|
183
|
+
const title = taskTitle(state);
|
|
184
|
+
const type = String(action.event_type || row.action || 'reviewed');
|
|
185
|
+
const reward = Number(row?.reward?.value ?? row?.reward);
|
|
186
|
+
if (type === 'revision_requested' || (Number.isFinite(reward) && reward < 0)) {
|
|
187
|
+
return `bounced ${title}: ${row.lesson || 'needs another pass'}`;
|
|
188
|
+
}
|
|
189
|
+
if (type === 'reviewed') return `validated ${title}`;
|
|
190
|
+
return `recorded feedback on ${title}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function scorecardSummary(row) {
|
|
194
|
+
const shipped = row && (row.what_shipped || row.summary || row.title || row.next_task_suggestion);
|
|
195
|
+
if (row && row.verify_passed === false) return `hit a failed check on ${shipped || 'the latest tick'}`;
|
|
196
|
+
if (shipped) return `finished ${shipped}`;
|
|
197
|
+
const reward = Number(row && row.reward);
|
|
198
|
+
if (Number.isFinite(reward) && reward < 0) return 'recorded a negative scorecard';
|
|
199
|
+
return 'recorded a scorecard';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function missionEventSummary(row) {
|
|
203
|
+
const payload = row && row.payload || {};
|
|
204
|
+
const type = String(row && (row.type || row.event || row.action) || 'mission_event');
|
|
205
|
+
const text = firstText(payload.summary, payload.objective, payload.reason, payload.next_action, row.summary, row.reason);
|
|
206
|
+
if (/start/.test(type)) return `started ${text || 'a mission'}`;
|
|
207
|
+
if (/tick/.test(type)) return `ran a mission tick${text ? `: ${text}` : ''}`;
|
|
208
|
+
if (/complete|done|ready/.test(type)) return `finished ${text || 'a mission'}`;
|
|
209
|
+
if (/pause|block|fail|error|stop/.test(type)) return `got blocked${text ? `: ${text}` : ''}`;
|
|
210
|
+
return text || 'updated a mission';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function missionStateSummary(mission) {
|
|
214
|
+
const objective = clip(mission.objective || mission.title || mission.next_action || 'the mission', 100);
|
|
215
|
+
const status = String(mission.status || 'active');
|
|
216
|
+
if (status === 'ready') return `finished ${objective}; proof is ready`;
|
|
217
|
+
if (status === 'paused' || status === 'blocked') return `paused ${objective}; waiting on operator`;
|
|
218
|
+
if (status === 'complete' || status === 'done') return `completed ${objective}`;
|
|
219
|
+
return `is working on ${objective}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function tickSummary(tick) {
|
|
223
|
+
const text = firstText(
|
|
224
|
+
tick.summary,
|
|
225
|
+
tick.result,
|
|
226
|
+
tick.reason,
|
|
227
|
+
tick.atris2 && tick.atris2.summary,
|
|
228
|
+
tick.claude && tick.claude.summary,
|
|
229
|
+
tick.atris2 && tick.atris2.receipt_text,
|
|
230
|
+
tick.claude && tick.claude.receipt_text,
|
|
231
|
+
);
|
|
232
|
+
const pass = tick.verifier_passed === true || tick.verifier_result?.passed === true;
|
|
233
|
+
const fail = tick.verifier_passed === false || tick.verifier_result?.passed === false || /fail|error|blocked/i.test(tick.status || tick.reason || '');
|
|
234
|
+
if (fail) return text ? `hit a failed check: ${text}` : 'hit a failed check during a mission tick';
|
|
235
|
+
if (pass && text) return `${text}; checks passed`;
|
|
236
|
+
return text || 'ran a mission tick';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function landingSummary(result) {
|
|
240
|
+
const landing = result && result.landing || {};
|
|
241
|
+
return firstText(landing.changed, landing.happened, landing.summary, result.summary, result.reason) || 'recorded a mission landing';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function sanitizeAgent(value) {
|
|
245
|
+
let text = String(value || '').trim();
|
|
246
|
+
if (!text) return 'team';
|
|
247
|
+
if (text.includes('/')) text = text.split('/')[0];
|
|
248
|
+
text = text.replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
249
|
+
return clip(text || 'team', 32);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function stripLeadingAgent(summary, agent) {
|
|
253
|
+
const cleanAgent = String(agent || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
254
|
+
if (!cleanAgent) return summary;
|
|
255
|
+
return String(summary || '').replace(new RegExp(`^${cleanAgent}\\s+`, 'i'), '');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function sanitizeSummary(value, agent = '') {
|
|
259
|
+
let text = sentence(stripLeadingAgent(value, agent));
|
|
260
|
+
text = text
|
|
261
|
+
.replace(/\b[0-9A-HJKMNP-TV-Z]{20,26}\b/g, 'the item')
|
|
262
|
+
.replace(/\b[A-Z]{2,12}-\d+\b/g, 'the task')
|
|
263
|
+
.replace(/\bmission-[a-z0-9][a-z0-9-]{8,}\b/ig, 'the mission')
|
|
264
|
+
.replace(/\b(?:refs\/heads\/|origin\/)?(?:codex|agent|feature|fix|chore|bugfix)\/[A-Za-z0-9._/-]+/g, 'the branch')
|
|
265
|
+
.replace(/(^|\s)~\/[^\s,;:)]+/g, '$1the workspace')
|
|
266
|
+
.replace(/(^|\s)\/[^\s,;:)]+/g, '$1the workspace')
|
|
267
|
+
.replace(/\b(?:\.{1,2}\/)?(?:\.atris|atris\/runs|commands|lib|test|bin|scripts)\/[^\s,;:)]+/g, 'the workspace')
|
|
268
|
+
.replace(/\s+/g, ' ')
|
|
269
|
+
.trim();
|
|
270
|
+
if (!/[.!?]$/.test(text)) text += '.';
|
|
271
|
+
return clip(text);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function makeEvent({ ts, agent, event, summary, raw, source, root }) {
|
|
275
|
+
const ms = timestampMs(ts);
|
|
276
|
+
if (!ms) return null;
|
|
277
|
+
const cleanAgent = sanitizeAgent(agent);
|
|
278
|
+
const cleanSummary = sanitizeSummary(summary, cleanAgent);
|
|
279
|
+
return {
|
|
280
|
+
ts: new Date(ms).toISOString(),
|
|
281
|
+
ms,
|
|
282
|
+
agent: cleanAgent,
|
|
283
|
+
event: String(event || source || 'event'),
|
|
284
|
+
summary: cleanSummary,
|
|
285
|
+
raw: raw || null,
|
|
286
|
+
source: source || 'state',
|
|
287
|
+
root: root || null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function eventKey(record) {
|
|
292
|
+
const raw = record.raw || {};
|
|
293
|
+
return [
|
|
294
|
+
record.event,
|
|
295
|
+
record.ts,
|
|
296
|
+
record.agent,
|
|
297
|
+
raw.event_id || raw.episode_id || raw.receipt_path || raw.file || raw.path || raw.task_id || raw.mission_id || '',
|
|
298
|
+
raw.index != null ? raw.index : '',
|
|
299
|
+
record.summary,
|
|
300
|
+
].join('|');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function pushEvent(events, input) {
|
|
304
|
+
const event = makeEvent(input);
|
|
305
|
+
if (event) events.push({ ...event, key: eventKey(event) });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function statePaths(root) {
|
|
309
|
+
const stateDir = path.join(root, '.atris', 'state');
|
|
310
|
+
return {
|
|
311
|
+
stateDir,
|
|
312
|
+
tasksProjection: path.join(stateDir, 'tasks.projection.json'),
|
|
313
|
+
missions: path.join(stateDir, 'missions.jsonl'),
|
|
314
|
+
missionEvents: path.join(stateDir, 'mission_events.jsonl'),
|
|
315
|
+
scorecards: path.join(stateDir, 'scorecards.jsonl'),
|
|
316
|
+
taskEpisodes: path.join(stateDir, 'task_episodes.jsonl'),
|
|
317
|
+
runsDir: path.join(root, 'atris', 'runs'),
|
|
318
|
+
sidecar: path.join(root, '.atris', 'agent-worktree.json'),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function readMissionMap(root, deps) {
|
|
323
|
+
const map = new Map();
|
|
324
|
+
for (const mission of readJsonl(statePaths(root).missions, deps, { tail: 1000 })) {
|
|
325
|
+
if (mission && mission.id) map.set(String(mission.id), mission);
|
|
326
|
+
}
|
|
327
|
+
return map;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function collectTaskProjectionEvents(root, deps, events) {
|
|
331
|
+
const projection = readJson(statePaths(root).tasksProjection, deps, {});
|
|
332
|
+
const tasks = Array.isArray(projection && projection.tasks) ? projection.tasks : [];
|
|
333
|
+
for (const task of tasks) {
|
|
334
|
+
const taskEvents = Array.isArray(task.events) ? task.events : [];
|
|
335
|
+
if (!taskEvents.length && task.updated_at) {
|
|
336
|
+
pushEvent(events, {
|
|
337
|
+
ts: task.updated_at,
|
|
338
|
+
agent: task.claimed_by || task.assigned_to || task.metadata?.assigned_to || 'team',
|
|
339
|
+
event: `task_${task.status || 'updated'}`,
|
|
340
|
+
summary: taskStateSummary(task),
|
|
341
|
+
raw: { source: 'tasks.projection.json', task },
|
|
342
|
+
source: 'task',
|
|
343
|
+
root,
|
|
344
|
+
});
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (const taskEvent of taskEvents) {
|
|
348
|
+
pushEvent(events, {
|
|
349
|
+
ts: taskEvent.created_at,
|
|
350
|
+
agent: taskEvent.actor || task.claimed_by || task.assigned_to || task.metadata?.assigned_to || 'team',
|
|
351
|
+
event: `task_${taskEvent.event_type || 'event'}`,
|
|
352
|
+
summary: taskEventSummary(task, taskEvent),
|
|
353
|
+
raw: { source: 'tasks.projection.json', task_id: task.id, task_title: task.title, ...taskEvent },
|
|
354
|
+
source: 'task',
|
|
355
|
+
root,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function collectTaskEpisodeEvents(root, deps, events) {
|
|
362
|
+
for (const row of readJsonl(statePaths(root).taskEpisodes, deps)) {
|
|
363
|
+
pushEvent(events, {
|
|
364
|
+
ts: row.created_at || row.ts,
|
|
365
|
+
agent: row.action?.actor || row.state?.claimed_by || row.actor || 'team',
|
|
366
|
+
event: `task_${row.action?.event_type || row.action || 'episode'}`,
|
|
367
|
+
summary: taskEpisodeSummary(row),
|
|
368
|
+
raw: row,
|
|
369
|
+
source: 'task_episode',
|
|
370
|
+
root,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function collectScorecardEvents(root, deps, events) {
|
|
376
|
+
for (const row of readJsonl(statePaths(root).scorecards, deps)) {
|
|
377
|
+
pushEvent(events, {
|
|
378
|
+
ts: row.ts || row.created_at || row.updated_at,
|
|
379
|
+
agent: row.member || row.actor || row.source || 'team',
|
|
380
|
+
event: row.verify_passed === false ? 'scorecard_failed' : 'scorecard',
|
|
381
|
+
summary: scorecardSummary(row),
|
|
382
|
+
raw: row,
|
|
383
|
+
source: 'scorecard',
|
|
384
|
+
root,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function collectMissionEvents(root, deps, events) {
|
|
390
|
+
const paths = statePaths(root);
|
|
391
|
+
for (const mission of readJsonl(paths.missions, deps, { tail: 1000 })) {
|
|
392
|
+
const status = String(mission.status || '');
|
|
393
|
+
if (!ACTIVE_MISSION_STATES.has(status)) continue;
|
|
394
|
+
pushEvent(events, {
|
|
395
|
+
ts: mission.last_tick_at || mission.updated_at || mission.created_at || mission.started_at,
|
|
396
|
+
agent: mission.owner || mission.member || mission.runner || 'team',
|
|
397
|
+
event: `mission_${status || 'active'}`,
|
|
398
|
+
summary: missionStateSummary(mission),
|
|
399
|
+
raw: mission,
|
|
400
|
+
source: 'mission_state',
|
|
401
|
+
root,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
for (const row of readJsonl(paths.missionEvents, deps)) {
|
|
405
|
+
pushEvent(events, {
|
|
406
|
+
ts: row.at || row.ts || row.created_at,
|
|
407
|
+
agent: row.actor || row.owner || row.payload?.owner || 'team',
|
|
408
|
+
event: row.type || row.event || 'mission_event',
|
|
409
|
+
summary: missionEventSummary(row),
|
|
410
|
+
raw: row,
|
|
411
|
+
source: 'mission_event',
|
|
412
|
+
root,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function collectRunReceiptEvents(root, deps, events) {
|
|
418
|
+
const paths = statePaths(root);
|
|
419
|
+
const missionMap = readMissionMap(root, deps);
|
|
420
|
+
const files = listDir(paths.runsDir, deps)
|
|
421
|
+
.filter((file) => file.endsWith('.json'))
|
|
422
|
+
.sort();
|
|
423
|
+
for (const file of files.slice(-DEFAULT_TAIL_LIMIT)) {
|
|
424
|
+
const fullPath = path.join(paths.runsDir, file);
|
|
425
|
+
const receipt = readJson(fullPath, deps, null);
|
|
426
|
+
if (!receipt || receipt.schema !== 'atris.mission_receipt.v1') continue;
|
|
427
|
+
const mission = missionMap.get(String(receipt.mission_id || '')) || {};
|
|
428
|
+
const agent = receipt.owner || mission.owner || 'team';
|
|
429
|
+
const result = receipt.result || {};
|
|
430
|
+
if (result.tick) {
|
|
431
|
+
pushEvent(events, {
|
|
432
|
+
ts: result.tick.finished_at || result.tick.started_at || receipt.at,
|
|
433
|
+
agent,
|
|
434
|
+
event: result.kind || 'mission_tick',
|
|
435
|
+
summary: tickSummary(result.tick),
|
|
436
|
+
raw: { ...receipt, file: fullPath, result: { ...result, tick: result.tick } },
|
|
437
|
+
source: 'mission_receipt',
|
|
438
|
+
root,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (Array.isArray(result.ticks)) {
|
|
442
|
+
result.ticks.forEach((tick, index) => {
|
|
443
|
+
pushEvent(events, {
|
|
444
|
+
ts: tick.finished_at || tick.started_at || receipt.at,
|
|
445
|
+
agent,
|
|
446
|
+
event: 'mission_tick',
|
|
447
|
+
summary: tickSummary(tick),
|
|
448
|
+
raw: { ...receipt, file: fullPath, index, tick },
|
|
449
|
+
source: 'mission_receipt',
|
|
450
|
+
root,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (result.landing) {
|
|
455
|
+
pushEvent(events, {
|
|
456
|
+
ts: receipt.at || result.finished_at || result.created_at,
|
|
457
|
+
agent,
|
|
458
|
+
event: result.kind || 'mission_landing',
|
|
459
|
+
summary: landingSummary(result),
|
|
460
|
+
raw: { ...receipt, file: fullPath },
|
|
461
|
+
source: 'mission_landing',
|
|
462
|
+
root,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function candidateWorktreeDirs(root, deps) {
|
|
469
|
+
const dirs = new Set([
|
|
470
|
+
path.resolve(root, '..', '.agent-worktrees'),
|
|
471
|
+
path.join(path.dirname(root), '.agent-worktrees', path.basename(root)),
|
|
472
|
+
]);
|
|
473
|
+
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
474
|
+
if (sidecar && sidecar.workspace_root) {
|
|
475
|
+
dirs.add(path.join(path.dirname(sidecar.workspace_root), '.agent-worktrees', path.basename(sidecar.workspace_root)));
|
|
476
|
+
}
|
|
477
|
+
const parts = path.resolve(root).split(path.sep);
|
|
478
|
+
const idx = parts.lastIndexOf('.agent-worktrees');
|
|
479
|
+
if (idx >= 0 && parts[idx + 1]) {
|
|
480
|
+
dirs.add(parts.slice(0, idx + 2).join(path.sep) || path.sep);
|
|
481
|
+
}
|
|
482
|
+
return [...dirs];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function discoverAgentWorktreeRoots(root, deps) {
|
|
486
|
+
const found = new Set();
|
|
487
|
+
const scanOne = (dir, depth = 0) => {
|
|
488
|
+
if (!isDirectory(dir, deps) || depth > 1) return;
|
|
489
|
+
for (const name of listDir(dir, deps)) {
|
|
490
|
+
const child = path.join(dir, name);
|
|
491
|
+
if (!isDirectory(child, deps)) continue;
|
|
492
|
+
if (deps.existsSync(path.join(child, '.atris', 'agent-worktree.json'))) {
|
|
493
|
+
found.add(child);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
scanOne(child, depth + 1);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
for (const dir of candidateWorktreeDirs(root, deps)) scanOne(dir);
|
|
500
|
+
return [...found];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function gitWorktrees(root, deps) {
|
|
504
|
+
if (deps.listWorktrees) {
|
|
505
|
+
try {
|
|
506
|
+
return deps.listWorktrees(root) || [];
|
|
507
|
+
} catch {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
return listWorktrees(root) || [];
|
|
513
|
+
} catch {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function collectWorkspaceRoots(root, deps) {
|
|
519
|
+
const out = new Map();
|
|
520
|
+
const add = (workspaceRoot, reason) => {
|
|
521
|
+
if (!workspaceRoot) return;
|
|
522
|
+
const resolved = path.resolve(workspaceRoot);
|
|
523
|
+
if (!out.has(resolved)) out.set(resolved, { root: resolved, reason });
|
|
524
|
+
};
|
|
525
|
+
add(root, 'current');
|
|
526
|
+
for (const wt of gitWorktrees(root, deps)) add(wt.path, 'git_worktree');
|
|
527
|
+
for (const wtRoot of discoverAgentWorktreeRoots(root, deps)) add(wtRoot, 'agent_worktree_dir');
|
|
528
|
+
return [...out.values()];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function worktreeOwner(root, deps) {
|
|
532
|
+
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
533
|
+
if (sidecar) return sidecar.owner || sidecar.member || sidecar.agent || '';
|
|
534
|
+
const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-owner$'], deps);
|
|
535
|
+
if (result.status === 0) {
|
|
536
|
+
const line = String(result.stdout || '').split(/\r?\n/).find(Boolean);
|
|
537
|
+
if (line) return line.split(/\s+/).slice(1).join(' ');
|
|
538
|
+
}
|
|
539
|
+
return '';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function worktreeTask(root, deps) {
|
|
543
|
+
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
544
|
+
if (sidecar && sidecar.task) return sidecar.task;
|
|
545
|
+
const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-task$'], deps);
|
|
546
|
+
if (result.status === 0) {
|
|
547
|
+
const line = String(result.stdout || '').split(/\r?\n/).find(Boolean);
|
|
548
|
+
if (line) return line.split(/\s+/).slice(1).join(' ');
|
|
549
|
+
}
|
|
550
|
+
return '';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function collectWorktreeActivityEvents(root, deps, events) {
|
|
554
|
+
const roots = collectWorkspaceRoots(root, deps);
|
|
555
|
+
for (const item of roots) {
|
|
556
|
+
const owner = worktreeOwner(item.root, deps);
|
|
557
|
+
if (!owner) continue;
|
|
558
|
+
const log = runGit(item.root, ['log', '-1', '--format=%cI%x09%an%x09%s'], deps);
|
|
559
|
+
if (log.status !== 0 || !String(log.stdout || '').trim()) continue;
|
|
560
|
+
const [ts, author, subject] = String(log.stdout || '').trim().split('\t');
|
|
561
|
+
pushEvent(events, {
|
|
562
|
+
ts,
|
|
563
|
+
agent: owner || author || 'agent',
|
|
564
|
+
event: 'worktree_commit',
|
|
565
|
+
summary: `committed ${subject || worktreeTask(item.root, deps) || 'worktree changes'}`,
|
|
566
|
+
raw: { root: item.root, reason: item.reason, owner, task: worktreeTask(item.root, deps), author, subject },
|
|
567
|
+
source: 'worktree',
|
|
568
|
+
root: item.root,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function collectLandingStateEvent(root, deps, events, nowMs) {
|
|
574
|
+
try {
|
|
575
|
+
const board = collectBoard(root);
|
|
576
|
+
const summary = board && board.summary || {};
|
|
577
|
+
const active = Number(summary.active || 0);
|
|
578
|
+
const due = Number(summary.due || 0);
|
|
579
|
+
const landed = Number(summary.landed || 0);
|
|
580
|
+
const worktrees = Number(summary.worktrees || 0);
|
|
581
|
+
if (!active && !due && !landed && !worktrees) return;
|
|
582
|
+
const pieces = [];
|
|
583
|
+
if (active) pieces.push(`${active} active`);
|
|
584
|
+
if (due) pieces.push(`${due} overdue`);
|
|
585
|
+
if (landed) pieces.push(`${landed} landed and ready to clear`);
|
|
586
|
+
if (worktrees) pieces.push(`${worktrees} side copies`);
|
|
587
|
+
pushEvent(events, {
|
|
588
|
+
ts: nowMs,
|
|
589
|
+
agent: 'landing',
|
|
590
|
+
event: 'landing_state',
|
|
591
|
+
summary: `landing board has ${pieces.join(', ')}`,
|
|
592
|
+
raw: board,
|
|
593
|
+
source: 'landing',
|
|
594
|
+
root,
|
|
595
|
+
});
|
|
596
|
+
} catch {
|
|
597
|
+
// Landing state is advisory; stream should still run outside git repos.
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', nowMs = Date.now(), deps = defaultDeps() } = {}) {
|
|
602
|
+
const allDeps = { ...defaultDeps(), ...deps };
|
|
603
|
+
const events = [];
|
|
604
|
+
const roots = collectWorkspaceRoots(root, allDeps);
|
|
605
|
+
for (const item of roots) {
|
|
606
|
+
collectMissionEvents(item.root, allDeps, events);
|
|
607
|
+
collectRunReceiptEvents(item.root, allDeps, events);
|
|
608
|
+
collectTaskProjectionEvents(item.root, allDeps, events);
|
|
609
|
+
collectTaskEpisodeEvents(item.root, allDeps, events);
|
|
610
|
+
collectScorecardEvents(item.root, allDeps, events);
|
|
611
|
+
}
|
|
612
|
+
collectWorktreeActivityEvents(root, allDeps, events);
|
|
613
|
+
collectLandingStateEvent(root, allDeps, events, nowMs);
|
|
614
|
+
const wantedAgent = agent ? sanitizeAgent(agent).toLowerCase() : '';
|
|
615
|
+
const byKey = new Map();
|
|
616
|
+
for (const event of events) {
|
|
617
|
+
if (sinceMs && event.ms < sinceMs) continue;
|
|
618
|
+
if (wantedAgent && event.agent.toLowerCase() !== wantedAgent) continue;
|
|
619
|
+
byKey.set(event.key, event);
|
|
620
|
+
}
|
|
621
|
+
return [...byKey.values()].sort((a, b) => (a.ms - b.ms) || a.event.localeCompare(b.event));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function waitingOnOperator(tasks) {
|
|
625
|
+
return tasks.filter((task) => {
|
|
626
|
+
const review = task.review || {};
|
|
627
|
+
const metadata = task.metadata || {};
|
|
628
|
+
const handoff = review.handoff || metadata.handoff || {};
|
|
629
|
+
const action = String(handoff.next_action || metadata.next_action || review.next_action || '');
|
|
630
|
+
const approval = String(review.approval_status || metadata.approval_status || '');
|
|
631
|
+
return task.status === 'blocked'
|
|
632
|
+
|| action === 'human_accept_waiting'
|
|
633
|
+
|| /human|operator|approval|waiting/i.test(approval)
|
|
634
|
+
|| metadata.agent_certified === true
|
|
635
|
+
|| review.agent_certified === true;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function collectSnapshot({ root = process.cwd(), deps = defaultDeps() } = {}) {
|
|
640
|
+
const allDeps = { ...defaultDeps(), ...deps };
|
|
641
|
+
const active = new Map();
|
|
642
|
+
const waiting = [];
|
|
643
|
+
for (const item of collectWorkspaceRoots(root, allDeps)) {
|
|
644
|
+
const projection = readJson(statePaths(item.root).tasksProjection, allDeps, {});
|
|
645
|
+
const tasks = Array.isArray(projection && projection.tasks) ? projection.tasks : [];
|
|
646
|
+
for (const task of tasks) {
|
|
647
|
+
const owner = task.claimed_by || task.assigned_to || task.metadata?.assigned_to || '';
|
|
648
|
+
if (owner && ACTIVE_TASK_STATES.has(String(task.status || ''))) active.set(sanitizeAgent(owner), taskTitle(task));
|
|
649
|
+
}
|
|
650
|
+
waiting.push(...waitingOnOperator(tasks));
|
|
651
|
+
for (const mission of readJsonl(statePaths(item.root).missions, allDeps, { tail: 1000 })) {
|
|
652
|
+
if (!ACTIVE_MISSION_STATES.has(String(mission.status || ''))) continue;
|
|
653
|
+
active.set(sanitizeAgent(mission.owner || mission.member || 'team'), clip(mission.objective || mission.next_action || 'mission work', 90));
|
|
654
|
+
}
|
|
655
|
+
const owner = worktreeOwner(item.root, allDeps);
|
|
656
|
+
if (owner) active.set(sanitizeAgent(owner), clip(worktreeTask(item.root, allDeps) || 'worktree changes', 90));
|
|
657
|
+
}
|
|
658
|
+
let landingWait = 0;
|
|
659
|
+
try {
|
|
660
|
+
const board = collectBoard(root);
|
|
661
|
+
landingWait = Number(board?.summary?.due || 0);
|
|
662
|
+
} catch {}
|
|
663
|
+
const activeRows = [...active.entries()];
|
|
664
|
+
const lines = [];
|
|
665
|
+
lines.push(`Team stream: ${activeRows.length} active agent${activeRows.length === 1 ? '' : 's'}.`);
|
|
666
|
+
const shown = activeRows.slice(0, 3).map(([agent, work]) => `${agent}: ${sanitizeSummary(work, agent).replace(/[.!?]$/, '')}`);
|
|
667
|
+
const more = activeRows.length > shown.length ? `, +${activeRows.length - shown.length} more` : '';
|
|
668
|
+
lines.push(`On now: ${shown.length ? `${shown.join('; ')}${more}.` : 'no active work found on disk.'}`);
|
|
669
|
+
const waits = [];
|
|
670
|
+
if (waiting.length) waits.push(`${waiting.length} review${waiting.length === 1 ? '' : 's'}`);
|
|
671
|
+
if (landingWait) waits.push(`${landingWait} overdue landing${landingWait === 1 ? '' : 's'}`);
|
|
672
|
+
lines.push(`Waiting on operator: ${waits.length ? waits.join(', ') : 'nothing obvious'}.`);
|
|
673
|
+
lines.push('Watching missions, task events, scorecards, landings, and worktrees. Ctrl-C exits.');
|
|
674
|
+
return { active_agents: activeRows.length, active: activeRows, waiting_operator: waiting.length, landing_wait: landingWait, lines };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function formatTs(ts) {
|
|
678
|
+
const iso = isoFrom(ts);
|
|
679
|
+
return iso ? iso.replace('T', ' ').replace(/\.\d{3}Z$/, 'Z') : String(ts || '');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function renderTextLine(record, { raw = false } = {}) {
|
|
683
|
+
const line = `${formatTs(record.ts)} ${sanitizeAgent(record.agent)} ${sanitizeSummary(record.summary, record.agent)}`;
|
|
684
|
+
if (!raw) return line;
|
|
685
|
+
return `${line} raw=${JSON.stringify(record.raw)}`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function renderJsonLine(record) {
|
|
689
|
+
return JSON.stringify({
|
|
690
|
+
ts: record.ts,
|
|
691
|
+
agent: sanitizeAgent(record.agent),
|
|
692
|
+
event: record.event,
|
|
693
|
+
summary: sanitizeSummary(record.summary, record.agent),
|
|
694
|
+
raw: record.raw || null,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function renderRecords(records, opts = {}) {
|
|
699
|
+
return records.map((record) => (opts.json ? renderJsonLine(record) : renderTextLine(record, opts))).join('\n');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function snapshotRecords(snapshot, nowMs) {
|
|
703
|
+
return snapshot.lines.map((line, index) => makeEvent({
|
|
704
|
+
ts: nowMs,
|
|
705
|
+
agent: 'team',
|
|
706
|
+
event: 'snapshot',
|
|
707
|
+
summary: line,
|
|
708
|
+
raw: { line: index + 1, snapshot: { active_agents: snapshot.active_agents, waiting_operator: snapshot.waiting_operator, landing_wait: snapshot.landing_wait } },
|
|
709
|
+
source: 'snapshot',
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function parseSince(value, nowMs = Date.now()) {
|
|
714
|
+
const raw = String(value || '').trim();
|
|
715
|
+
if (!raw) return 0;
|
|
716
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)(s|m|h|d)$/i);
|
|
717
|
+
if (match) {
|
|
718
|
+
const n = Number(match[1]);
|
|
719
|
+
const unit = match[2].toLowerCase();
|
|
720
|
+
const scale = unit === 's' ? 1000 : unit === 'm' ? 60000 : unit === 'h' ? 3600000 : 86400000;
|
|
721
|
+
return nowMs - n * scale;
|
|
722
|
+
}
|
|
723
|
+
const parsed = Date.parse(raw);
|
|
724
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
725
|
+
throw new Error(`invalid --since value: ${value}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function parseArgs(args = [], nowMs = Date.now()) {
|
|
729
|
+
const sinceRaw = readFlag(args, '--since', '');
|
|
730
|
+
const intervalRaw = readFlag(args, '--interval', '');
|
|
731
|
+
const interval = Number(intervalRaw);
|
|
732
|
+
return {
|
|
733
|
+
agent: readFlag(args, '--agent', ''),
|
|
734
|
+
sinceMs: parseSince(sinceRaw, nowMs),
|
|
735
|
+
json: hasFlag(args, '--json'),
|
|
736
|
+
raw: hasFlag(args, '--raw'),
|
|
737
|
+
once: hasFlag(args, '--once'),
|
|
738
|
+
intervalMs: Number.isFinite(interval) && interval > 0 ? Math.max(250, interval * 1000) : DEFAULT_INTERVAL_MS,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function createStreamState({ root, sinceMs = 0, agent = '', deps = defaultDeps(), nowMs = Date.now(), skipExisting = false } = {}) {
|
|
743
|
+
const seen = new Set();
|
|
744
|
+
if (skipExisting) {
|
|
745
|
+
for (const event of collectStreamEvents({ root, sinceMs, agent, deps, nowMs })) seen.add(event.key);
|
|
746
|
+
}
|
|
747
|
+
return { seen };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function pollStreamOnce(state, { root, sinceMs = 0, agent = '', deps = defaultDeps(), nowMs = Date.now() } = {}) {
|
|
751
|
+
const events = collectStreamEvents({ root, sinceMs, agent, deps, nowMs });
|
|
752
|
+
const fresh = [];
|
|
753
|
+
for (const event of events) {
|
|
754
|
+
if (state.seen.has(event.key)) continue;
|
|
755
|
+
state.seen.add(event.key);
|
|
756
|
+
fresh.push(event);
|
|
757
|
+
}
|
|
758
|
+
return fresh;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function showHelp() {
|
|
762
|
+
console.log('');
|
|
763
|
+
console.log('atris stream - watch the whole team work live');
|
|
764
|
+
console.log('');
|
|
765
|
+
console.log(' atris stream');
|
|
766
|
+
console.log(' atris stream --agent codex');
|
|
767
|
+
console.log(' atris stream --since 1h');
|
|
768
|
+
console.log(' atris stream --json');
|
|
769
|
+
console.log(' atris stream --raw');
|
|
770
|
+
console.log('');
|
|
771
|
+
console.log('Default lines hide ids, branch names, and paths. Use --raw or --json for source records.');
|
|
772
|
+
console.log('');
|
|
773
|
+
return 0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function watchDirs(root, deps, onChange) {
|
|
777
|
+
if (typeof deps.watch !== 'function') return [];
|
|
778
|
+
const dirs = [statePaths(root).stateDir, statePaths(root).runsDir].filter((dir) => deps.existsSync(dir));
|
|
779
|
+
const watchers = [];
|
|
780
|
+
for (const dir of dirs) {
|
|
781
|
+
try {
|
|
782
|
+
watchers.push(deps.watch(dir, { persistent: true }, onChange));
|
|
783
|
+
} catch {}
|
|
784
|
+
}
|
|
785
|
+
return watchers;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function streamCommand(args = [], deps = defaultDeps()) {
|
|
789
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') return showHelp();
|
|
790
|
+
const allDeps = { ...defaultDeps(), ...deps };
|
|
791
|
+
const root = repoRoot(process.cwd(), allDeps);
|
|
792
|
+
const nowMs = allDeps.now();
|
|
793
|
+
const opts = parseArgs(args, nowMs);
|
|
794
|
+
const snapshot = collectSnapshot({ root, deps: allDeps });
|
|
795
|
+
const initialSince = opts.sinceMs || 0;
|
|
796
|
+
|
|
797
|
+
if (opts.json) {
|
|
798
|
+
for (const record of snapshotRecords(snapshot, nowMs)) console.log(renderJsonLine(record));
|
|
799
|
+
} else {
|
|
800
|
+
for (const line of snapshot.lines) console.log(line);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const state = createStreamState({
|
|
804
|
+
root,
|
|
805
|
+
sinceMs: initialSince,
|
|
806
|
+
agent: opts.agent,
|
|
807
|
+
deps: allDeps,
|
|
808
|
+
nowMs,
|
|
809
|
+
skipExisting: !opts.sinceMs,
|
|
810
|
+
});
|
|
811
|
+
const backfill = opts.sinceMs
|
|
812
|
+
? pollStreamOnce(state, { root, sinceMs: opts.sinceMs, agent: opts.agent, deps: allDeps, nowMs })
|
|
813
|
+
: [];
|
|
814
|
+
if (backfill.length) console.log(renderRecords(backfill, opts));
|
|
815
|
+
if (opts.once) return 0;
|
|
816
|
+
|
|
817
|
+
let closed = false;
|
|
818
|
+
let polling = false;
|
|
819
|
+
const poll = () => {
|
|
820
|
+
if (closed || polling) return;
|
|
821
|
+
polling = true;
|
|
822
|
+
try {
|
|
823
|
+
const fresh = pollStreamOnce(state, { root, sinceMs: opts.sinceMs || (nowMs - 24 * 60 * 60 * 1000), agent: opts.agent, deps: allDeps, nowMs: allDeps.now() });
|
|
824
|
+
if (fresh.length) console.log(renderRecords(fresh, opts));
|
|
825
|
+
} finally {
|
|
826
|
+
polling = false;
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const interval = setInterval(poll, opts.intervalMs);
|
|
830
|
+
const watchers = watchDirs(root, allDeps, () => setTimeout(poll, 25));
|
|
831
|
+
return new Promise((resolve) => {
|
|
832
|
+
const stop = () => {
|
|
833
|
+
if (closed) return;
|
|
834
|
+
closed = true;
|
|
835
|
+
clearInterval(interval);
|
|
836
|
+
for (const watcher of watchers) {
|
|
837
|
+
try { watcher.close(); } catch {}
|
|
838
|
+
}
|
|
839
|
+
process.stdout.write('\n');
|
|
840
|
+
resolve(0);
|
|
841
|
+
};
|
|
842
|
+
process.once('SIGINT', stop);
|
|
843
|
+
process.once('SIGTERM', stop);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
module.exports = {
|
|
848
|
+
collectSnapshot,
|
|
849
|
+
collectStreamEvents,
|
|
850
|
+
createStreamState,
|
|
851
|
+
parseArgs,
|
|
852
|
+
parseSince,
|
|
853
|
+
pollStreamOnce,
|
|
854
|
+
renderJsonLine,
|
|
855
|
+
renderRecords,
|
|
856
|
+
renderTextLine,
|
|
857
|
+
repoRoot,
|
|
858
|
+
sanitizeSummary,
|
|
859
|
+
snapshotRecords,
|
|
860
|
+
streamCommand,
|
|
861
|
+
};
|