atris 3.15.13 → 3.15.22
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 +84 -8
- package/README.md +5 -1
- package/atris/AGENTS.md +46 -1
- package/atris/CLAUDE.md +36 -1
- package/atris/GEMINI.md +14 -1
- package/atris/atris.md +12 -1
- package/atris/atrisDev.md +3 -2
- package/atris/context/README.md +11 -0
- package/atris/features/company-brain-sync/validate.md +5 -5
- package/atris/learnings.jsonl +1 -0
- package/atris/policies/atris-design.md +2 -0
- package/atris/skills/aeo/SKILL.md +2 -2
- package/atris/skills/atris/SKILL.md +15 -62
- package/atris/skills/design/SKILL.md +2 -0
- package/atris/skills/imessage/SKILL.md +19 -2
- package/atris/skills/loop/SKILL.md +6 -5
- package/atris/skills/magic-inbox/SKILL.md +1 -1
- package/atris/team/_template/MEMBER.md +23 -1
- package/atris/team/brainstormer/START_HERE.md +6 -0
- package/atris/team/executor/MEMBER.md +13 -0
- package/atris/team/executor/START_HERE.md +6 -0
- package/atris/team/launcher/START_HERE.md +6 -0
- package/atris/team/mission-lead/MEMBER.md +39 -0
- package/atris/team/mission-lead/MISSION.md +33 -0
- package/atris/team/mission-lead/START_HERE.md +6 -0
- package/atris/team/navigator/MEMBER.md +11 -0
- package/atris/team/navigator/START_HERE.md +6 -0
- package/atris/team/opus-overnight/MEMBER.md +39 -0
- package/atris/team/opus-overnight/MISSION.md +61 -0
- package/atris/team/opus-overnight/START_HERE.md +6 -0
- package/atris/team/opus-overnight/STEERING.md +35 -0
- package/atris/team/researcher/START_HERE.md +6 -0
- package/atris/team/validator/MEMBER.md +26 -6
- package/atris/team/validator/START_HERE.md +6 -0
- package/atris/wiki/concepts/agent-activation-contract.md +79 -0
- package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
- package/atris/wiki/index.md +27 -0
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
- package/atris.md +49 -13
- package/bin/atris.js +660 -22
- package/commands/activate.js +12 -3
- package/commands/aeo.js +1 -1
- package/commands/align.js +10 -10
- package/commands/analytics.js +9 -4
- package/commands/app.js +2 -0
- package/commands/apps.js +276 -0
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +74 -5
- package/commands/brain.js +536 -61
- package/commands/brainstorm.js +12 -12
- package/commands/business-sync.js +142 -24
- package/commands/clean.js +9 -6
- package/commands/codex-goal.js +311 -0
- package/commands/errors.js +11 -1
- package/commands/feedback.js +55 -17
- package/commands/fork.js +2 -2
- package/commands/gm.js +376 -0
- package/commands/init.js +80 -3
- package/commands/integrations.js +524 -0
- package/commands/learn.js +25 -16
- package/commands/lesson.js +41 -0
- package/commands/lifecycle.js +2 -2
- package/commands/member.js +2416 -9
- package/commands/mission.js +1776 -0
- package/commands/now.js +48 -7
- package/commands/play.js +425 -0
- package/commands/publish.js +2 -1
- package/commands/pull.js +72 -29
- package/commands/push.js +199 -17
- package/commands/review.js +51 -13
- package/commands/skill.js +2 -2
- package/commands/soul.js +19 -13
- package/commands/status.js +6 -1
- package/commands/sync.js +5 -4
- package/commands/task.js +1041 -147
- package/commands/terminal.js +5 -5
- package/commands/verify.js +7 -5
- package/commands/visualize.js +7 -0
- package/commands/wiki.js +53 -16
- package/commands/workflow.js +298 -54
- package/commands/workspace-clean.js +1 -1
- package/commands/worktree.js +468 -0
- package/commands/xp.js +1608 -0
- package/lib/manifest.js +34 -4
- package/lib/scorecard.js +3 -2
- package/lib/task-db.js +408 -27
- package/lib/todo-fallback.js +28 -2
- package/lib/todo.js +5 -3
- package/package.json +23 -2
- package/utils/update-check.js +51 -1
package/commands/xp.js
ADDED
|
@@ -0,0 +1,1608 @@
|
|
|
1
|
+
const { ensureValidCredentials } = require('../utils/auth');
|
|
2
|
+
const { apiRequestJson } = require('../utils/api');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_GRAPH_DAYS = 365;
|
|
10
|
+
const INTENSITY_CHARS = [' ', '.', ':', '*', '#'];
|
|
11
|
+
const ROW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
12
|
+
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
13
|
+
const CAREER_XP_RECEIPTS_FILE = path.join('.atris', 'state', 'career_xp_receipts.jsonl');
|
|
14
|
+
const CAREER_XP_PROJECTION_FILE = path.join('.atris', 'state', 'career_xp.projection.json');
|
|
15
|
+
const CAREER_XP_CURSOR_FILE = path.join('.atris', 'state', 'career_xp.cursor.json');
|
|
16
|
+
const CAREER_XP_SESSIONS_DIR = path.join('.atris', 'state', 'career_xp_sessions');
|
|
17
|
+
const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
18
|
+
const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
19
|
+
const AGENT_XP_LABEL = 'AgentXP';
|
|
20
|
+
const LEVEL_XP = 1000;
|
|
21
|
+
const RECEIPT_CHAIN_VERSION = 'atris.career_xp_receipt_chain.v1';
|
|
22
|
+
const XP_STATE_FILES = new Set([
|
|
23
|
+
path.basename(TASK_EPISODES_FILE),
|
|
24
|
+
path.basename(CAREER_XP_RECEIPTS_FILE),
|
|
25
|
+
path.basename(CAREER_XP_PROJECTION_FILE),
|
|
26
|
+
]);
|
|
27
|
+
const SEARCH_EXCLUDED_DIRS = new Set([
|
|
28
|
+
'.git',
|
|
29
|
+
'node_modules',
|
|
30
|
+
'dist',
|
|
31
|
+
'build',
|
|
32
|
+
'.next',
|
|
33
|
+
'coverage',
|
|
34
|
+
'tmp',
|
|
35
|
+
'temp',
|
|
36
|
+
]);
|
|
37
|
+
const DEFAULT_SEARCH_DEPTH = 6;
|
|
38
|
+
|
|
39
|
+
function showHelp() {
|
|
40
|
+
console.log('Usage: atris xp [card|status|collect|session] [--json] [--workspace <path>] [--all] [--root <path>]');
|
|
41
|
+
console.log(' atris xp session [--since today|tonight|YYYY-MM-DD] [--until <time>] [--mission <text>] [--thread <id>] [--no-write]');
|
|
42
|
+
console.log(' atris xp [--json] [--local] [--workspace <path>] [--operator <name>]');
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('Show your AgentXP graph for the active Atris account.');
|
|
45
|
+
console.log('Use status to show account-level AgentXP across verified local ledgers.');
|
|
46
|
+
console.log('Use collect or status --local to project accepted task proof in the current workspace.');
|
|
47
|
+
console.log('Use session to encapsulate the current work window into a local XP capsule.');
|
|
48
|
+
console.log('Use status --all to explicitly aggregate verified local XP ledgers across workspaces.');
|
|
49
|
+
console.log('Use --local to render from proof receipts in the current workspace.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asNumber(value, fallback = 0) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatNumber(value) {
|
|
58
|
+
return asNumber(value).toLocaleString('en-US');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildContributionRows(days) {
|
|
62
|
+
const normalized = Array.isArray(days) ? days : [];
|
|
63
|
+
if (normalized.length === 0) {
|
|
64
|
+
return ROW_LABELS.map(label => `${label} `);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const first = new Date(`${normalized[0].date}T00:00:00Z`);
|
|
68
|
+
const pad = Number.isFinite(first.getTime()) ? first.getUTCDay() : 0;
|
|
69
|
+
const padded = [
|
|
70
|
+
...Array.from({ length: pad }, () => null),
|
|
71
|
+
...normalized,
|
|
72
|
+
];
|
|
73
|
+
while (padded.length % 7 !== 0) padded.push(null);
|
|
74
|
+
|
|
75
|
+
return ROW_LABELS.map((label, rowIndex) => {
|
|
76
|
+
let line = `${label} `;
|
|
77
|
+
for (let index = rowIndex; index < padded.length; index += 7) {
|
|
78
|
+
const day = padded[index];
|
|
79
|
+
const intensity = Math.max(0, Math.min(4, asNumber(day?.intensity)));
|
|
80
|
+
line += INTENSITY_CHARS[intensity];
|
|
81
|
+
}
|
|
82
|
+
return line;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function dateWindow(windowDays = DEFAULT_GRAPH_DAYS) {
|
|
87
|
+
const count = Math.max(1, asNumber(windowDays, DEFAULT_GRAPH_DAYS));
|
|
88
|
+
const end = new Date();
|
|
89
|
+
const dates = [];
|
|
90
|
+
for (let offset = count - 1; offset >= 0; offset -= 1) {
|
|
91
|
+
const date = new Date(end);
|
|
92
|
+
date.setDate(end.getDate() - offset);
|
|
93
|
+
const key = localDateKey(date);
|
|
94
|
+
if (key && dates[dates.length - 1] !== key) dates.push(key);
|
|
95
|
+
}
|
|
96
|
+
return dates;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function graphIntensity(xp, maxDailyXp) {
|
|
100
|
+
const total = asNumber(xp);
|
|
101
|
+
if (total <= 0) return 0;
|
|
102
|
+
const max = Math.max(1, asNumber(maxDailyXp, 1));
|
|
103
|
+
if (max <= 1) return 1;
|
|
104
|
+
const ratio = total / max;
|
|
105
|
+
if (ratio >= 0.75) return 4;
|
|
106
|
+
if (ratio >= 0.5) return 3;
|
|
107
|
+
if (ratio >= 0.25) return 2;
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function graphFromDailyTotals(dailyTotals, windowDays = DEFAULT_GRAPH_DAYS) {
|
|
112
|
+
const totals = dailyTotals instanceof Map
|
|
113
|
+
? dailyTotals
|
|
114
|
+
: new Map(Object.entries(dailyTotals || {}));
|
|
115
|
+
const dates = dateWindow(windowDays);
|
|
116
|
+
const maxDailyXp = dates.reduce((max, date) => Math.max(max, asNumber(totals.get(date))), 0);
|
|
117
|
+
const days = dates.map((date) => {
|
|
118
|
+
const xp = asNumber(totals.get(date));
|
|
119
|
+
return {
|
|
120
|
+
date,
|
|
121
|
+
xp,
|
|
122
|
+
total_xp: xp,
|
|
123
|
+
intensity: graphIntensity(xp, maxDailyXp),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
schema: 'atris.agent_xp_contribution_graph.v1',
|
|
128
|
+
metric_label: AGENT_XP_LABEL,
|
|
129
|
+
window_days: days.length,
|
|
130
|
+
total_xp: days.reduce((sum, day) => sum + day.xp, 0),
|
|
131
|
+
active_days: days.filter(day => day.xp > 0).length,
|
|
132
|
+
first_date: days[0]?.date || null,
|
|
133
|
+
last_date: days[days.length - 1]?.date || null,
|
|
134
|
+
days,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildAgentXpContributionGraph(receipts, windowDays = DEFAULT_GRAPH_DAYS) {
|
|
139
|
+
const totals = new Map();
|
|
140
|
+
for (const receipt of receipts || []) {
|
|
141
|
+
if (!receipt || receipt.outcome !== 'accepted') continue;
|
|
142
|
+
const xp = asNumber(receipt.xp);
|
|
143
|
+
if (xp <= 0) continue;
|
|
144
|
+
const date = localDateKey(receipt.accepted_at);
|
|
145
|
+
if (!date) continue;
|
|
146
|
+
totals.set(date, asNumber(totals.get(date)) + xp);
|
|
147
|
+
}
|
|
148
|
+
return graphFromDailyTotals(totals, windowDays);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function combineAgentXpContributionGraphs(graphs, windowDays = DEFAULT_GRAPH_DAYS) {
|
|
152
|
+
const totals = new Map();
|
|
153
|
+
for (const graph of graphs || []) {
|
|
154
|
+
for (const day of graph?.days || []) {
|
|
155
|
+
const date = day?.date;
|
|
156
|
+
if (!date) continue;
|
|
157
|
+
const xp = asNumber(day.xp ?? day.total_xp);
|
|
158
|
+
if (xp <= 0) continue;
|
|
159
|
+
totals.set(date, asNumber(totals.get(date)) + xp);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return graphFromDailyTotals(totals, windowDays);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function currentForm(payload) {
|
|
166
|
+
const arenas = payload.current_form_by_arena || {};
|
|
167
|
+
const local = arenas.local_workspace || {};
|
|
168
|
+
const ovr = asNumber(local.ovr || local.current_form);
|
|
169
|
+
if (!ovr) return null;
|
|
170
|
+
return {
|
|
171
|
+
ovr,
|
|
172
|
+
visibleStats: Array.isArray(local.visible_stats) ? local.visible_stats : [],
|
|
173
|
+
leaderboardEligible: Boolean(local.leaderboard_eligible),
|
|
174
|
+
integrityStatus: local.integrity_status || 'unknown',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readFlag(args, name, fallback = null) {
|
|
179
|
+
const inline = args.find(arg => arg.startsWith(`${name}=`));
|
|
180
|
+
if (inline) {
|
|
181
|
+
return inline.slice(name.length + 1);
|
|
182
|
+
}
|
|
183
|
+
const index = args.indexOf(name);
|
|
184
|
+
if (index >= 0 && args[index + 1] && !args[index + 1].startsWith('--')) {
|
|
185
|
+
return args[index + 1];
|
|
186
|
+
}
|
|
187
|
+
return fallback;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readFlagValues(args, names) {
|
|
191
|
+
const wanted = Array.isArray(names) ? names : [names];
|
|
192
|
+
const values = [];
|
|
193
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
194
|
+
const arg = args[index];
|
|
195
|
+
for (const name of wanted) {
|
|
196
|
+
if (arg === name && args[index + 1] && !args[index + 1].startsWith('--')) {
|
|
197
|
+
values.push(args[index + 1]);
|
|
198
|
+
} else if (arg.startsWith(`${name}=`)) {
|
|
199
|
+
values.push(arg.slice(name.length + 1));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return values.filter(Boolean);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasFlag(args, name) {
|
|
207
|
+
return args.includes(name) || args.some(arg => arg.startsWith(`${name}=`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function levelFromXp(careerXp) {
|
|
211
|
+
return Math.max(1, Math.floor(asNumber(careerXp) / LEVEL_XP) + 1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function localDateKey(value, timeZone = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC') {
|
|
215
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
216
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
217
|
+
try {
|
|
218
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
219
|
+
timeZone,
|
|
220
|
+
year: 'numeric',
|
|
221
|
+
month: '2-digit',
|
|
222
|
+
day: '2-digit',
|
|
223
|
+
}).format(date);
|
|
224
|
+
} catch (_) {
|
|
225
|
+
return date.toISOString().slice(0, 10);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseJsonlContent(content, options = {}) {
|
|
230
|
+
const allowPartialTail = Boolean(options.allowPartialTail);
|
|
231
|
+
const rows = [];
|
|
232
|
+
const hasTerminalNewline = /\r?\n$/.test(content);
|
|
233
|
+
const lines = content.split('\n');
|
|
234
|
+
let parsedBytes = 0;
|
|
235
|
+
let skippedPartialTail = false;
|
|
236
|
+
let skippedPartialTailBytes = 0;
|
|
237
|
+
|
|
238
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
239
|
+
const rawLine = lines[index];
|
|
240
|
+
const isLast = index === lines.length - 1;
|
|
241
|
+
const lineHadNewline = !isLast || hasTerminalNewline;
|
|
242
|
+
const lineBytes = isLast && hasTerminalNewline && rawLine === ''
|
|
243
|
+
? 0
|
|
244
|
+
: Buffer.byteLength(rawLine, 'utf8') + (lineHadNewline ? 1 : 0);
|
|
245
|
+
const line = rawLine.trim();
|
|
246
|
+
|
|
247
|
+
if (!line) {
|
|
248
|
+
parsedBytes += lineBytes;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
rows.push(JSON.parse(line));
|
|
254
|
+
parsedBytes += lineBytes;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (allowPartialTail && isLast && !hasTerminalNewline) {
|
|
257
|
+
skippedPartialTail = true;
|
|
258
|
+
skippedPartialTailBytes = lineBytes;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
rows,
|
|
267
|
+
parsedBytes,
|
|
268
|
+
skippedPartialTail,
|
|
269
|
+
skippedPartialTailBytes,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readJsonl(filePath, options = {}) {
|
|
274
|
+
if (!fs.existsSync(filePath)) return [];
|
|
275
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
276
|
+
return parseJsonlContent(content, options).rows;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function writeJson(filePath, payload) {
|
|
280
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
281
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function appendJsonl(filePath, rows) {
|
|
285
|
+
if (!rows.length) return;
|
|
286
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
287
|
+
fs.appendFileSync(filePath, rows.map(row => JSON.stringify(row)).join('\n') + '\n', 'utf8');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function writeJsonl(filePath, rows) {
|
|
291
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
292
|
+
fs.writeFileSync(filePath, rows.length ? rows.map(row => JSON.stringify(row)).join('\n') + '\n' : '', 'utf8');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function canonicalJson(value) {
|
|
296
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
297
|
+
if (value && typeof value === 'object') {
|
|
298
|
+
return `{${Object.keys(value).sort().map((key) => {
|
|
299
|
+
const item = value[key];
|
|
300
|
+
if (item === undefined) return null;
|
|
301
|
+
return `${JSON.stringify(key)}:${canonicalJson(item)}`;
|
|
302
|
+
}).filter(Boolean).join(',')}}`;
|
|
303
|
+
}
|
|
304
|
+
return JSON.stringify(value);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function sha256(value) {
|
|
308
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function hashPayload(value) {
|
|
312
|
+
return sha256(canonicalJson(value));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readJsonFile(filePath, fallback = null) {
|
|
316
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
317
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function fileSize(filePath) {
|
|
321
|
+
try {
|
|
322
|
+
return fs.statSync(filePath).size;
|
|
323
|
+
} catch (_) {
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function timestampMs(value) {
|
|
329
|
+
if (value === null || value === undefined || value === '') return null;
|
|
330
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
331
|
+
const numeric = Number(value);
|
|
332
|
+
if (Number.isFinite(numeric) && String(value).trim() !== '') return numeric;
|
|
333
|
+
const parsed = new Date(value).getTime();
|
|
334
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isoFromMs(value) {
|
|
338
|
+
const ms = timestampMs(value);
|
|
339
|
+
return ms === null ? null : new Date(ms).toISOString();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function startOfToday() {
|
|
343
|
+
const date = new Date();
|
|
344
|
+
date.setHours(0, 0, 0, 0);
|
|
345
|
+
return date;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function startOfTonight() {
|
|
349
|
+
const date = new Date();
|
|
350
|
+
if (date.getHours() < 6) {
|
|
351
|
+
date.setDate(date.getDate() - 1);
|
|
352
|
+
}
|
|
353
|
+
date.setHours(18, 0, 0, 0);
|
|
354
|
+
return date;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseSessionBoundary(value, fallback) {
|
|
358
|
+
if (!value) return fallback;
|
|
359
|
+
const normalized = String(value).trim().toLowerCase();
|
|
360
|
+
if (!normalized) return fallback;
|
|
361
|
+
if (normalized === 'now') return new Date();
|
|
362
|
+
if (normalized === 'today') return startOfToday();
|
|
363
|
+
if (normalized === 'tonight') return startOfTonight();
|
|
364
|
+
if (normalized === 'yesterday') {
|
|
365
|
+
const date = startOfToday();
|
|
366
|
+
date.setDate(date.getDate() - 1);
|
|
367
|
+
return date;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalized);
|
|
371
|
+
if (dateOnly) {
|
|
372
|
+
return new Date(
|
|
373
|
+
Number(dateOnly[1]),
|
|
374
|
+
Number(dateOnly[2]) - 1,
|
|
375
|
+
Number(dateOnly[3]),
|
|
376
|
+
0,
|
|
377
|
+
0,
|
|
378
|
+
0,
|
|
379
|
+
0
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const parsed = new Date(value);
|
|
384
|
+
if (Number.isFinite(parsed.getTime())) return parsed;
|
|
385
|
+
throw new Error(`Invalid session time: ${value}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function inWindow(value, sinceMs, untilMs) {
|
|
389
|
+
const ms = timestampMs(value);
|
|
390
|
+
return ms !== null && ms >= sinceMs && ms <= untilMs;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function expandHome(filePath) {
|
|
394
|
+
if (!filePath) return filePath;
|
|
395
|
+
if (filePath === '~') return os.homedir();
|
|
396
|
+
if (filePath.startsWith('~/')) return path.join(os.homedir(), filePath.slice(2));
|
|
397
|
+
return filePath;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function sqlString(value) {
|
|
401
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function runSqliteJsonOptional(dbPath, sql) {
|
|
405
|
+
if (!dbPath || !fs.existsSync(dbPath)) return [];
|
|
406
|
+
const result = spawnSync('sqlite3', ['-readonly', '-json', dbPath, sql], { encoding: 'utf8' });
|
|
407
|
+
if (result.error || result.status !== 0) return [];
|
|
408
|
+
const out = String(result.stdout || '').trim();
|
|
409
|
+
if (!out) return [];
|
|
410
|
+
try {
|
|
411
|
+
return JSON.parse(out);
|
|
412
|
+
} catch (_) {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function receiptHashBase(receipt, previousHash) {
|
|
418
|
+
const base = {
|
|
419
|
+
...receipt,
|
|
420
|
+
chain_version: RECEIPT_CHAIN_VERSION,
|
|
421
|
+
previous_receipt_hash: previousHash || null,
|
|
422
|
+
};
|
|
423
|
+
delete base.receipt_hash;
|
|
424
|
+
return base;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function receiptDuplicateComparable(receipt) {
|
|
428
|
+
const comparable = { ...(receipt || {}) };
|
|
429
|
+
delete comparable.chain_version;
|
|
430
|
+
delete comparable.previous_receipt_hash;
|
|
431
|
+
delete comparable.receipt_hash;
|
|
432
|
+
return comparable;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function receiptLabel(receipt) {
|
|
436
|
+
const title = String(receipt?.title || '').trim();
|
|
437
|
+
if (title) return title;
|
|
438
|
+
const task = String(receipt?.source_task_id || '').trim();
|
|
439
|
+
if (task) return `task ${task.slice(0, 8)}`;
|
|
440
|
+
const source = String(receipt?.source_type || receipt?.source || '').trim();
|
|
441
|
+
return source || 'accepted proof';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function withReceiptIntegrity(receipt, previousHash) {
|
|
445
|
+
const base = receiptHashBase(receipt, previousHash);
|
|
446
|
+
return {
|
|
447
|
+
...base,
|
|
448
|
+
receipt_hash: hashPayload(base),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function normalizeReceiptChain(receipts, { allowLegacyUpgrade = true } = {}) {
|
|
453
|
+
const seen = new Map();
|
|
454
|
+
const errors = [];
|
|
455
|
+
const dedupedReceipts = [];
|
|
456
|
+
const normalized = [];
|
|
457
|
+
let previousHash = null;
|
|
458
|
+
let upgraded = false;
|
|
459
|
+
|
|
460
|
+
for (const receipt of receipts) {
|
|
461
|
+
if (!receipt || typeof receipt !== 'object') {
|
|
462
|
+
errors.push('invalid_receipt');
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (!receipt.receipt_id) errors.push('missing_receipt_id');
|
|
466
|
+
if (receipt.receipt_id && seen.has(receipt.receipt_id)) {
|
|
467
|
+
const first = seen.get(receipt.receipt_id);
|
|
468
|
+
if (canonicalJson(receiptDuplicateComparable(first)) === canonicalJson(receiptDuplicateComparable(receipt))) {
|
|
469
|
+
dedupedReceipts.push(receiptLabel(receipt));
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
errors.push(`conflicting_duplicate_receipt:${receiptLabel(receipt)}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const expected = withReceiptIntegrity(receipt, previousHash);
|
|
476
|
+
const hadIntegrity = Boolean(receipt.receipt_hash && receipt.chain_version && Object.prototype.hasOwnProperty.call(receipt, 'previous_receipt_hash'));
|
|
477
|
+
if (hadIntegrity) {
|
|
478
|
+
if (receipt.chain_version !== RECEIPT_CHAIN_VERSION) {
|
|
479
|
+
errors.push(`receipt_chain_version_mismatch:${receiptLabel(receipt)}`);
|
|
480
|
+
}
|
|
481
|
+
if ((receipt.previous_receipt_hash || null) !== (previousHash || null)) {
|
|
482
|
+
errors.push(`receipt_previous_hash_mismatch:${receiptLabel(receipt)}`);
|
|
483
|
+
}
|
|
484
|
+
if (receipt.receipt_hash !== expected.receipt_hash) {
|
|
485
|
+
errors.push(`receipt_hash_mismatch:${receiptLabel(receipt)}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!hadIntegrity && !allowLegacyUpgrade) {
|
|
489
|
+
errors.push(`missing_receipt_integrity:${receiptLabel(receipt)}`);
|
|
490
|
+
}
|
|
491
|
+
if (!hadIntegrity && allowLegacyUpgrade) upgraded = true;
|
|
492
|
+
|
|
493
|
+
normalized.push(expected);
|
|
494
|
+
previousHash = expected.receipt_hash;
|
|
495
|
+
if (receipt.receipt_id && !seen.has(receipt.receipt_id)) seen.set(receipt.receipt_id, receipt);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
receipts: normalized,
|
|
500
|
+
upgraded,
|
|
501
|
+
deduped: dedupedReceipts.length > 0,
|
|
502
|
+
integrity: {
|
|
503
|
+
status: errors.length ? 'tampered' : 'verified',
|
|
504
|
+
chain_version: RECEIPT_CHAIN_VERSION,
|
|
505
|
+
receipts_count: normalized.length,
|
|
506
|
+
head_hash: previousHash,
|
|
507
|
+
errors,
|
|
508
|
+
deduped_receipts: dedupedReceipts,
|
|
509
|
+
local_trust: 'tamper_evident_not_attested',
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function alignJsonlReadStart(filePath, start) {
|
|
515
|
+
if (!start || start <= 0) return 0;
|
|
516
|
+
const fd = fs.openSync(filePath, 'r');
|
|
517
|
+
try {
|
|
518
|
+
const previous = Buffer.alloc(1);
|
|
519
|
+
fs.readSync(fd, previous, 0, 1, start - 1);
|
|
520
|
+
if (previous[0] === 0x0a) return start;
|
|
521
|
+
|
|
522
|
+
const current = Buffer.alloc(1);
|
|
523
|
+
const currentBytes = fs.readSync(fd, current, 0, 1, start);
|
|
524
|
+
if (previous[0] === 0x0d && currentBytes === 1 && current[0] === 0x0a) {
|
|
525
|
+
return start + 1;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const chunkSize = 64 * 1024;
|
|
529
|
+
let scanEnd = start;
|
|
530
|
+
while (scanEnd > 0) {
|
|
531
|
+
const scanStart = Math.max(0, scanEnd - chunkSize);
|
|
532
|
+
const buffer = Buffer.alloc(scanEnd - scanStart);
|
|
533
|
+
fs.readSync(fd, buffer, 0, buffer.length, scanStart);
|
|
534
|
+
for (let index = buffer.length - 1; index >= 0; index -= 1) {
|
|
535
|
+
if (buffer[index] === 0x0a) return scanStart + index + 1;
|
|
536
|
+
}
|
|
537
|
+
scanEnd = scanStart;
|
|
538
|
+
}
|
|
539
|
+
return 0;
|
|
540
|
+
} finally {
|
|
541
|
+
fs.closeSync(fd);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function readTaskEpisodeTail(episodePath, cursorPath, options = {}) {
|
|
546
|
+
const stat = fs.existsSync(episodePath) ? fs.statSync(episodePath) : null;
|
|
547
|
+
const forceReset = Boolean(options.forceReset);
|
|
548
|
+
const cursor = forceReset ? null : readJsonFile(cursorPath, null);
|
|
549
|
+
const previousBytes = Number(cursor?.bytes_read || 0);
|
|
550
|
+
const size = stat ? stat.size : 0;
|
|
551
|
+
const reset = forceReset || !cursor || cursor.source_path !== episodePath || size < previousBytes;
|
|
552
|
+
const start = reset || !stat ? 0 : alignJsonlReadStart(episodePath, previousBytes);
|
|
553
|
+
if (!stat || size === start) {
|
|
554
|
+
return {
|
|
555
|
+
episodes: [],
|
|
556
|
+
cursor: {
|
|
557
|
+
schema: 'atris.career_xp_cursor.v1',
|
|
558
|
+
source_path: episodePath,
|
|
559
|
+
bytes_read: size,
|
|
560
|
+
source_size: size,
|
|
561
|
+
last_episode_id: cursor?.last_episode_id || null,
|
|
562
|
+
updated_at: new Date().toISOString(),
|
|
563
|
+
reset,
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const fd = fs.openSync(episodePath, 'r');
|
|
569
|
+
try {
|
|
570
|
+
const buffer = Buffer.alloc(size - start);
|
|
571
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
572
|
+
const parsed = parseJsonlContent(buffer.toString('utf8'), { allowPartialTail: true });
|
|
573
|
+
const episodes = parsed.rows;
|
|
574
|
+
const bytesRead = start + parsed.parsedBytes;
|
|
575
|
+
return {
|
|
576
|
+
episodes,
|
|
577
|
+
cursor: {
|
|
578
|
+
schema: 'atris.career_xp_cursor.v1',
|
|
579
|
+
source_path: episodePath,
|
|
580
|
+
bytes_read: bytesRead,
|
|
581
|
+
source_size: size,
|
|
582
|
+
last_episode_id: episodes.length ? episodes[episodes.length - 1].episode_id || null : cursor?.last_episode_id || null,
|
|
583
|
+
updated_at: new Date().toISOString(),
|
|
584
|
+
reset,
|
|
585
|
+
skipped_partial_tail: parsed.skippedPartialTail,
|
|
586
|
+
skipped_partial_tail_bytes: parsed.skippedPartialTailBytes,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
} finally {
|
|
590
|
+
fs.closeSync(fd);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function receiptFromTaskEpisode(episode) {
|
|
595
|
+
const doneForXp = String(episode?.state?.status || '').toLowerCase() === 'done';
|
|
596
|
+
const hasExplicitCareerXp = episode?.career_xp && typeof episode.career_xp === 'object';
|
|
597
|
+
const eligible = doneForXp && (
|
|
598
|
+
episode?.career_xp?.eligible === true
|
|
599
|
+
|| (!hasExplicitCareerXp && episode?.rl?.label === 'accepted')
|
|
600
|
+
);
|
|
601
|
+
const proof = String(episode?.proof || '').trim();
|
|
602
|
+
const reward = asNumber(episode?.career_xp?.reward ?? episode?.reward?.value, 0);
|
|
603
|
+
if (!eligible || !proof || reward <= 0 || !episode?.episode_id) return null;
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
schema: 'atris.career_xp_receipt.v1',
|
|
607
|
+
receipt_id: `task_review:${episode.episode_id}`,
|
|
608
|
+
source: 'atris-cli',
|
|
609
|
+
source_type: 'task_review',
|
|
610
|
+
source_task_id: episode.task_id || null,
|
|
611
|
+
source_episode_id: episode.episode_id,
|
|
612
|
+
workspace_root: episode.workspace_root || null,
|
|
613
|
+
actor: episode.action?.actor || null,
|
|
614
|
+
outcome: 'accepted',
|
|
615
|
+
xp: reward,
|
|
616
|
+
reward,
|
|
617
|
+
proof,
|
|
618
|
+
proof_ref: proof,
|
|
619
|
+
source_episode_hash: hashPayload(episode),
|
|
620
|
+
title: episode.state?.title || null,
|
|
621
|
+
goal: episode.goal || null,
|
|
622
|
+
accepted_at: episode.created_at || new Date().toISOString(),
|
|
623
|
+
tokens_used: Number.isFinite(Number(episode.tokens_used)) ? Number(episode.tokens_used) : null,
|
|
624
|
+
duration_seconds: Number.isFinite(Number(episode.duration_seconds)) ? Number(episode.duration_seconds) : null,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function latestReceipt(receipts) {
|
|
629
|
+
return receipts
|
|
630
|
+
.slice()
|
|
631
|
+
.sort((a, b) => new Date(b.accepted_at).getTime() - new Date(a.accepted_at).getTime())[0] || null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function countBySource(receipts) {
|
|
635
|
+
return receipts.reduce((acc, receipt) => {
|
|
636
|
+
const source = receipt.source || 'unknown';
|
|
637
|
+
acc[source] = (acc[source] || 0) + 1;
|
|
638
|
+
return acc;
|
|
639
|
+
}, {});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function buildCareerXpProjection(receipts, workspace, integrity = {}) {
|
|
643
|
+
const trusted = integrity.status === 'tampered' ? [] : receipts;
|
|
644
|
+
const accepted = trusted
|
|
645
|
+
.filter(receipt => receipt && receipt.outcome === 'accepted' && asNumber(receipt.xp) > 0)
|
|
646
|
+
.sort((a, b) => new Date(a.accepted_at).getTime() - new Date(b.accepted_at).getTime());
|
|
647
|
+
const totalXp = accepted.reduce((sum, receipt) => sum + asNumber(receipt.xp), 0);
|
|
648
|
+
const today = localDateKey(new Date());
|
|
649
|
+
const todayXp = accepted
|
|
650
|
+
.filter(receipt => localDateKey(receipt.accepted_at) === today)
|
|
651
|
+
.reduce((sum, receipt) => sum + asNumber(receipt.xp), 0);
|
|
652
|
+
const level = levelFromXp(totalXp);
|
|
653
|
+
const levelBase = (level - 1) * LEVEL_XP;
|
|
654
|
+
const currentLevelXp = totalXp - levelBase;
|
|
655
|
+
const remainingXp = Math.max(0, (level * LEVEL_XP) - totalXp);
|
|
656
|
+
const nextLevelProgress = {
|
|
657
|
+
level,
|
|
658
|
+
next_level: level + 1,
|
|
659
|
+
current_xp: currentLevelXp,
|
|
660
|
+
required_xp: LEVEL_XP,
|
|
661
|
+
remaining_xp: remainingXp,
|
|
662
|
+
percent: Math.round((currentLevelXp / LEVEL_XP) * 1000) / 10,
|
|
663
|
+
};
|
|
664
|
+
const latest = latestReceipt(accepted);
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
schema: 'atris.career_xp_projection.v1',
|
|
668
|
+
generated_at: new Date().toISOString(),
|
|
669
|
+
workspace_root: workspace,
|
|
670
|
+
metric_label: AGENT_XP_LABEL,
|
|
671
|
+
agent_xp: totalXp,
|
|
672
|
+
total_agent_xp: totalXp,
|
|
673
|
+
today_agent_xp: todayXp,
|
|
674
|
+
career_xp: totalXp,
|
|
675
|
+
total_xp: totalXp,
|
|
676
|
+
today_xp: todayXp,
|
|
677
|
+
level,
|
|
678
|
+
leaderboard_eligible: false,
|
|
679
|
+
integrity_status: integrity.status || 'unknown',
|
|
680
|
+
next_level_progress: nextLevelProgress,
|
|
681
|
+
career: {
|
|
682
|
+
level,
|
|
683
|
+
card_label: 'Career Card',
|
|
684
|
+
progress_label: 'Career Progress',
|
|
685
|
+
next_level_progress: nextLevelProgress,
|
|
686
|
+
},
|
|
687
|
+
contribution_graph: buildAgentXpContributionGraph(accepted),
|
|
688
|
+
receipts_count: accepted.length,
|
|
689
|
+
sources: countBySource(accepted),
|
|
690
|
+
latest_accepted_proof: latest ? {
|
|
691
|
+
label: receiptLabel(latest),
|
|
692
|
+
receipt_id: latest.receipt_id,
|
|
693
|
+
source: latest.source,
|
|
694
|
+
source_task_id: latest.source_task_id,
|
|
695
|
+
source_episode_id: latest.source_episode_id,
|
|
696
|
+
title: latest.title,
|
|
697
|
+
proof: latest.proof,
|
|
698
|
+
xp: latest.xp,
|
|
699
|
+
reward: latest.reward,
|
|
700
|
+
actor: latest.actor,
|
|
701
|
+
accepted_at: latest.accepted_at,
|
|
702
|
+
goal: latest.goal || null,
|
|
703
|
+
} : null,
|
|
704
|
+
ledger: {
|
|
705
|
+
receipts_path: path.join(workspace, CAREER_XP_RECEIPTS_FILE),
|
|
706
|
+
projection_path: path.join(workspace, CAREER_XP_PROJECTION_FILE),
|
|
707
|
+
cursor_path: path.join(workspace, CAREER_XP_CURSOR_FILE),
|
|
708
|
+
},
|
|
709
|
+
integrity: {
|
|
710
|
+
status: integrity.status || 'unknown',
|
|
711
|
+
chain_version: integrity.chain_version || RECEIPT_CHAIN_VERSION,
|
|
712
|
+
head_hash: integrity.head_hash || null,
|
|
713
|
+
receipts_count: integrity.receipts_count ?? accepted.length,
|
|
714
|
+
errors: integrity.errors || [],
|
|
715
|
+
deduped_receipts: integrity.deduped_receipts || [],
|
|
716
|
+
local_trust: integrity.local_trust || 'tamper_evident_not_attested',
|
|
717
|
+
cursor: integrity.cursor || null,
|
|
718
|
+
note: 'Local XP is tamper-evident. Public trust still requires cloud/notary attestation.',
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function collectLocalXpProjectionState(args = [], { write = true } = {}) {
|
|
724
|
+
const workspace = path.resolve(readFlag(args, '--workspace', process.cwd()));
|
|
725
|
+
const episodePath = path.join(workspace, TASK_EPISODES_FILE);
|
|
726
|
+
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
727
|
+
const projectionPath = path.join(workspace, CAREER_XP_PROJECTION_FILE);
|
|
728
|
+
const cursorPath = path.join(workspace, CAREER_XP_CURSOR_FILE);
|
|
729
|
+
let existingChain = normalizeReceiptChain(readJsonl(receiptsPath));
|
|
730
|
+
if ((existingChain.upgraded || existingChain.deduped) && existingChain.integrity.status === 'verified') {
|
|
731
|
+
if (write) writeJsonl(receiptsPath, existingChain.receipts);
|
|
732
|
+
existingChain = normalizeReceiptChain(existingChain.receipts);
|
|
733
|
+
}
|
|
734
|
+
if (existingChain.integrity.status === 'tampered') {
|
|
735
|
+
const projection = buildCareerXpProjection([], workspace, existingChain.integrity);
|
|
736
|
+
if (write) writeJson(projectionPath, projection);
|
|
737
|
+
return {
|
|
738
|
+
projection: {
|
|
739
|
+
...projection,
|
|
740
|
+
collected_receipts: 0,
|
|
741
|
+
},
|
|
742
|
+
receipts: [],
|
|
743
|
+
newReceipts: [],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const replayFromStart = existingChain.receipts.length === 0
|
|
748
|
+
&& fileSize(episodePath) > 0
|
|
749
|
+
&& fileSize(receiptsPath) === 0;
|
|
750
|
+
const tail = readTaskEpisodeTail(episodePath, cursorPath, { forceReset: replayFromStart });
|
|
751
|
+
const seen = new Set(existingChain.receipts.map(receipt => receipt.receipt_id).filter(Boolean));
|
|
752
|
+
let previousHash = existingChain.integrity.head_hash || null;
|
|
753
|
+
const newReceipts = tail.episodes
|
|
754
|
+
.map(receiptFromTaskEpisode)
|
|
755
|
+
.filter(Boolean)
|
|
756
|
+
.filter((receipt) => {
|
|
757
|
+
if (seen.has(receipt.receipt_id)) return false;
|
|
758
|
+
seen.add(receipt.receipt_id);
|
|
759
|
+
return true;
|
|
760
|
+
})
|
|
761
|
+
.map((receipt) => {
|
|
762
|
+
const signed = withReceiptIntegrity(receipt, previousHash);
|
|
763
|
+
previousHash = signed.receipt_hash;
|
|
764
|
+
return signed;
|
|
765
|
+
});
|
|
766
|
+
const receipts = [...existingChain.receipts, ...newReceipts];
|
|
767
|
+
const finalChain = normalizeReceiptChain(receipts);
|
|
768
|
+
const finalIntegrity = {
|
|
769
|
+
...finalChain.integrity,
|
|
770
|
+
cursor: tail.cursor,
|
|
771
|
+
};
|
|
772
|
+
const projection = buildCareerXpProjection(
|
|
773
|
+
finalIntegrity.status === 'tampered' ? [] : finalChain.receipts,
|
|
774
|
+
workspace,
|
|
775
|
+
finalIntegrity,
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
if (write) {
|
|
779
|
+
appendJsonl(receiptsPath, newReceipts);
|
|
780
|
+
writeJson(cursorPath, tail.cursor);
|
|
781
|
+
writeJson(projectionPath, projection);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
projection: {
|
|
786
|
+
...projection,
|
|
787
|
+
collected_receipts: newReceipts.length,
|
|
788
|
+
},
|
|
789
|
+
receipts: finalChain.receipts,
|
|
790
|
+
newReceipts,
|
|
791
|
+
cursor: tail.cursor,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function collectLocalXpProjection(args = []) {
|
|
796
|
+
return collectLocalXpProjectionState(args, { write: true }).projection;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function uniquePaths(paths) {
|
|
800
|
+
const seen = new Set();
|
|
801
|
+
return paths
|
|
802
|
+
.map(item => path.resolve(item))
|
|
803
|
+
.filter((item) => {
|
|
804
|
+
if (seen.has(item)) return false;
|
|
805
|
+
seen.add(item);
|
|
806
|
+
return true;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function stateWorkspaceForFile(filePath) {
|
|
811
|
+
const stateDir = path.dirname(filePath);
|
|
812
|
+
if (path.basename(stateDir) !== 'state') return null;
|
|
813
|
+
const atrisDir = path.dirname(stateDir);
|
|
814
|
+
if (path.basename(atrisDir) !== '.atris') return null;
|
|
815
|
+
return path.dirname(atrisDir);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function discoverCareerXpWorkspaces(root, maxDepth = DEFAULT_SEARCH_DEPTH) {
|
|
819
|
+
const start = path.resolve(root);
|
|
820
|
+
if (!fs.existsSync(start)) return [];
|
|
821
|
+
const workspaces = new Set();
|
|
822
|
+
const stack = [{ dir: start, depth: 0 }];
|
|
823
|
+
|
|
824
|
+
while (stack.length) {
|
|
825
|
+
const { dir, depth } = stack.pop();
|
|
826
|
+
let entries;
|
|
827
|
+
try {
|
|
828
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
829
|
+
} catch (_) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
for (const entry of entries) {
|
|
834
|
+
const entryPath = path.join(dir, entry.name);
|
|
835
|
+
if (entry.isDirectory()) {
|
|
836
|
+
if (depth >= maxDepth || SEARCH_EXCLUDED_DIRS.has(entry.name)) continue;
|
|
837
|
+
stack.push({ dir: entryPath, depth: depth + 1 });
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (!entry.isFile() || !XP_STATE_FILES.has(entry.name)) continue;
|
|
841
|
+
const workspace = stateWorkspaceForFile(entryPath);
|
|
842
|
+
if (workspace) workspaces.add(workspace);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return Array.from(workspaces).sort();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function defaultAllSearchRoots(args = []) {
|
|
850
|
+
const explicitRoots = readFlagValues(args, ['--root', '--search-root']);
|
|
851
|
+
if (explicitRoots.length) return uniquePaths(explicitRoots);
|
|
852
|
+
|
|
853
|
+
const roots = [];
|
|
854
|
+
const workspace = readFlag(args, '--workspace', null);
|
|
855
|
+
if (workspace) roots.push(workspace);
|
|
856
|
+
roots.push(process.cwd());
|
|
857
|
+
roots.push(path.join(os.homedir(), 'arena'));
|
|
858
|
+
return uniquePaths(roots);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function workspaceName(workspace) {
|
|
862
|
+
return path.basename(workspace) || workspace;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function isVerifiedProjection(projection) {
|
|
866
|
+
return projection?.schema === 'atris.career_xp_projection.v1'
|
|
867
|
+
&& projection.integrity_status === 'verified'
|
|
868
|
+
&& projection.integrity?.status === 'verified';
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function buildAllCareerXpProjection(projections, searchRoots = []) {
|
|
872
|
+
const warnings = [];
|
|
873
|
+
const verified = [];
|
|
874
|
+
const workspaces = projections.map((item) => {
|
|
875
|
+
if (item.error) {
|
|
876
|
+
warnings.push({
|
|
877
|
+
workspace_root: item.workspace_root,
|
|
878
|
+
reason: item.error,
|
|
879
|
+
});
|
|
880
|
+
return {
|
|
881
|
+
workspace_root: item.workspace_root,
|
|
882
|
+
name: workspaceName(item.workspace_root),
|
|
883
|
+
included: false,
|
|
884
|
+
integrity_status: 'error',
|
|
885
|
+
error: item.error,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const projection = item.projection;
|
|
890
|
+
const included = isVerifiedProjection(projection);
|
|
891
|
+
if (included) {
|
|
892
|
+
verified.push(projection);
|
|
893
|
+
} else {
|
|
894
|
+
warnings.push({
|
|
895
|
+
workspace_root: item.workspace_root,
|
|
896
|
+
reason: `integrity:${projection?.integrity_status || projection?.integrity?.status || 'unknown'}`,
|
|
897
|
+
errors: projection?.integrity?.errors || [],
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
workspace_root: item.workspace_root,
|
|
903
|
+
name: workspaceName(item.workspace_root),
|
|
904
|
+
included,
|
|
905
|
+
total_xp: asNumber(projection?.total_xp),
|
|
906
|
+
today_xp: asNumber(projection?.today_xp),
|
|
907
|
+
level: asNumber(projection?.level, 1),
|
|
908
|
+
receipts_count: asNumber(projection?.receipts_count),
|
|
909
|
+
integrity_status: projection?.integrity_status || projection?.integrity?.status || 'unknown',
|
|
910
|
+
leaderboard_eligible: Boolean(projection?.leaderboard_eligible),
|
|
911
|
+
latest_accepted_proof: projection?.latest_accepted_proof || null,
|
|
912
|
+
ledger: projection?.ledger || null,
|
|
913
|
+
};
|
|
914
|
+
}).sort((a, b) => {
|
|
915
|
+
if (b.included !== a.included) return Number(b.included) - Number(a.included);
|
|
916
|
+
return b.total_xp - a.total_xp || a.name.localeCompare(b.name);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const totalXp = verified.reduce((sum, projection) => sum + asNumber(projection.total_xp), 0);
|
|
920
|
+
const todayXp = verified.reduce((sum, projection) => sum + asNumber(projection.today_xp), 0);
|
|
921
|
+
const level = levelFromXp(totalXp);
|
|
922
|
+
const levelBase = (level - 1) * LEVEL_XP;
|
|
923
|
+
const currentLevelXp = totalXp - levelBase;
|
|
924
|
+
const nextLevelProgress = {
|
|
925
|
+
level,
|
|
926
|
+
next_level: level + 1,
|
|
927
|
+
current_xp: currentLevelXp,
|
|
928
|
+
required_xp: LEVEL_XP,
|
|
929
|
+
remaining_xp: Math.max(0, (level * LEVEL_XP) - totalXp),
|
|
930
|
+
percent: Math.round((currentLevelXp / LEVEL_XP) * 1000) / 10,
|
|
931
|
+
};
|
|
932
|
+
const latest = verified
|
|
933
|
+
.map(projection => ({
|
|
934
|
+
...(projection.latest_accepted_proof || {}),
|
|
935
|
+
workspace_root: projection.workspace_root,
|
|
936
|
+
workspace_name: workspaceName(projection.workspace_root),
|
|
937
|
+
}))
|
|
938
|
+
.filter(proof => proof && proof.accepted_at)
|
|
939
|
+
.sort((a, b) => new Date(b.accepted_at).getTime() - new Date(a.accepted_at).getTime())[0] || null;
|
|
940
|
+
const contributionGraph = combineAgentXpContributionGraphs(
|
|
941
|
+
verified.map(projection => projection.contribution_graph),
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
schema: 'atris.career_xp_profile.v1',
|
|
946
|
+
generated_at: new Date().toISOString(),
|
|
947
|
+
search_roots: searchRoots,
|
|
948
|
+
workspace_count: workspaces.length,
|
|
949
|
+
verified_workspace_count: verified.length,
|
|
950
|
+
metric_label: AGENT_XP_LABEL,
|
|
951
|
+
agent_xp: totalXp,
|
|
952
|
+
total_agent_xp: totalXp,
|
|
953
|
+
today_agent_xp: todayXp,
|
|
954
|
+
career_xp: totalXp,
|
|
955
|
+
total_xp: totalXp,
|
|
956
|
+
today_xp: todayXp,
|
|
957
|
+
level,
|
|
958
|
+
leaderboard_eligible: false,
|
|
959
|
+
next_level_progress: nextLevelProgress,
|
|
960
|
+
career: {
|
|
961
|
+
level,
|
|
962
|
+
card_label: 'Career Card',
|
|
963
|
+
progress_label: 'Career Progress',
|
|
964
|
+
next_level_progress: nextLevelProgress,
|
|
965
|
+
},
|
|
966
|
+
contribution_graph: contributionGraph,
|
|
967
|
+
receipts_count: verified.reduce((sum, projection) => sum + asNumber(projection.receipts_count), 0),
|
|
968
|
+
latest_accepted_proof: latest,
|
|
969
|
+
workspaces,
|
|
970
|
+
integrity: {
|
|
971
|
+
status: warnings.length ? 'warnings' : 'verified',
|
|
972
|
+
warnings,
|
|
973
|
+
local_trust: 'workspace_local_tamper_evident_not_attested',
|
|
974
|
+
note: 'Only verified local ledgers are counted. Public trust still requires cloud/notary attestation.',
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function collectAllLocalXpProjection(args = []) {
|
|
980
|
+
const searchRoots = defaultAllSearchRoots(args).filter(root => fs.existsSync(root));
|
|
981
|
+
const explicitWorkspaces = readFlagValues(args, '--workspace');
|
|
982
|
+
const discovered = searchRoots.flatMap(root => discoverCareerXpWorkspaces(root));
|
|
983
|
+
const workspaces = uniquePaths([...explicitWorkspaces, ...discovered]);
|
|
984
|
+
const projections = workspaces.map((workspace) => {
|
|
985
|
+
try {
|
|
986
|
+
return {
|
|
987
|
+
workspace_root: workspace,
|
|
988
|
+
projection: collectLocalXpProjection(['--workspace', workspace]),
|
|
989
|
+
};
|
|
990
|
+
} catch (error) {
|
|
991
|
+
return {
|
|
992
|
+
workspace_root: workspace,
|
|
993
|
+
error: error.message,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
return buildAllCareerXpProjection(projections, searchRoots);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function readTaskProjectionState(workspace) {
|
|
1002
|
+
const projectionPath = path.join(workspace, TASK_PROJECTION_FILE);
|
|
1003
|
+
try {
|
|
1004
|
+
const projection = readJsonFile(projectionPath, { tasks: [] });
|
|
1005
|
+
return {
|
|
1006
|
+
tasks: Array.isArray(projection?.tasks) ? projection.tasks : [],
|
|
1007
|
+
warning: null,
|
|
1008
|
+
};
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return {
|
|
1011
|
+
tasks: [],
|
|
1012
|
+
warning: `task_projection_unreadable:${error.message}`,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function readTaskProjection(workspace) {
|
|
1018
|
+
return readTaskProjectionState(workspace).tasks;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function taskRef(task) {
|
|
1022
|
+
return task?.display_id || task?.legacy_ref || task?.id || 'task';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function taskTimes(task) {
|
|
1026
|
+
const times = [
|
|
1027
|
+
task?.created_at,
|
|
1028
|
+
task?.updated_at,
|
|
1029
|
+
task?.done_at,
|
|
1030
|
+
task?.review?.reviewed_at,
|
|
1031
|
+
task?.review?.accepted_at,
|
|
1032
|
+
task?.review?.revised_at,
|
|
1033
|
+
task?.metadata?.agent_reviewed_at,
|
|
1034
|
+
task?.metadata?.accepted_at,
|
|
1035
|
+
task?.metadata?.human_revision_at,
|
|
1036
|
+
];
|
|
1037
|
+
for (const event of task?.events || []) times.push(event.created_at);
|
|
1038
|
+
for (const message of task?.messages || []) times.push(message.created_at);
|
|
1039
|
+
return times
|
|
1040
|
+
.map(timestampMs)
|
|
1041
|
+
.filter(ms => ms !== null);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function taskTouchedInWindow(task, sinceMs, untilMs) {
|
|
1045
|
+
return taskTimes(task).some(ms => ms >= sinceMs && ms <= untilMs);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function latestTaskMs(task) {
|
|
1049
|
+
const times = taskTimes(task);
|
|
1050
|
+
return times.length ? Math.max(...times) : null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function taskApprovalStatus(task) {
|
|
1054
|
+
return task?.review?.approval_status || task?.metadata?.approval_status || null;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function textExcerpt(value, limit = 220) {
|
|
1058
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
1059
|
+
if (text.length <= limit) return text || null;
|
|
1060
|
+
return `${text.slice(0, limit - 3)}...`;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function compactTask(task) {
|
|
1064
|
+
const proof = task?.review?.proof || task?.metadata?.latest_agent_proof || null;
|
|
1065
|
+
return {
|
|
1066
|
+
task_id: taskRef(task),
|
|
1067
|
+
id: task?.id || null,
|
|
1068
|
+
title: task?.title || null,
|
|
1069
|
+
status: task?.status || null,
|
|
1070
|
+
tag: task?.tag || null,
|
|
1071
|
+
claimed_by: task?.claimed_by || null,
|
|
1072
|
+
approval_status: taskApprovalStatus(task),
|
|
1073
|
+
summary: task?.review?.summary || null,
|
|
1074
|
+
proof_excerpt: textExcerpt(proof),
|
|
1075
|
+
has_proof: Boolean(proof),
|
|
1076
|
+
updated_at: isoFromMs(latestTaskMs(task)),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function receiptMatchesTask(receipt, task) {
|
|
1081
|
+
const sourceTaskId = String(receipt?.source_task_id || '');
|
|
1082
|
+
if (!sourceTaskId) return false;
|
|
1083
|
+
return [task?.id, task?.display_id, task?.legacy_ref]
|
|
1084
|
+
.filter(Boolean)
|
|
1085
|
+
.map(String)
|
|
1086
|
+
.includes(sourceTaskId);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function compactReceipt(receipt, task = null, options = {}) {
|
|
1090
|
+
const proof = receipt?.proof || null;
|
|
1091
|
+
const compact = {
|
|
1092
|
+
receipt_id: receipt?.receipt_id || null,
|
|
1093
|
+
task_id: task ? taskRef(task) : receipt?.source_task_id || null,
|
|
1094
|
+
source_task_id: receipt?.source_task_id || null,
|
|
1095
|
+
title: receipt?.title || task?.title || null,
|
|
1096
|
+
proof_excerpt: textExcerpt(proof),
|
|
1097
|
+
has_proof: Boolean(proof),
|
|
1098
|
+
xp: asNumber(receipt?.xp),
|
|
1099
|
+
actor: receipt?.actor || null,
|
|
1100
|
+
accepted_at: receipt?.accepted_at || null,
|
|
1101
|
+
goal: receipt?.goal || null,
|
|
1102
|
+
};
|
|
1103
|
+
if (options.fullProof) {
|
|
1104
|
+
compact.proof = proof;
|
|
1105
|
+
}
|
|
1106
|
+
return compact;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function compactGoal(goal) {
|
|
1110
|
+
if (!goal) return null;
|
|
1111
|
+
if (typeof goal === 'string') {
|
|
1112
|
+
const label = goal.trim();
|
|
1113
|
+
return label ? { label } : null;
|
|
1114
|
+
}
|
|
1115
|
+
const label = goal.objective || goal.title || goal.condition || goal.id || goal.goal_id || null;
|
|
1116
|
+
if (!label) return null;
|
|
1117
|
+
return {
|
|
1118
|
+
provider: goal.provider || null,
|
|
1119
|
+
thread_id: goal.thread_id || null,
|
|
1120
|
+
goal_id: goal.id || goal.goal_id || null,
|
|
1121
|
+
label,
|
|
1122
|
+
status: goal.status || null,
|
|
1123
|
+
met: typeof goal.met === 'boolean' ? goal.met : null,
|
|
1124
|
+
tokens_used: Number.isFinite(Number(goal.tokens_used)) ? Number(goal.tokens_used) : null,
|
|
1125
|
+
time_used_seconds: Number.isFinite(Number(goal.time_used_seconds)) ? Number(goal.time_used_seconds) : null,
|
|
1126
|
+
created_at: goal.created_at || isoFromMs(goal.created_at_ms),
|
|
1127
|
+
updated_at: goal.updated_at || isoFromMs(goal.updated_at_ms),
|
|
1128
|
+
workspace_path: goal.thread_cwd || goal.workspace_path || null,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function uniqueGoals(goals) {
|
|
1133
|
+
const seen = new Set();
|
|
1134
|
+
return goals
|
|
1135
|
+
.map(compactGoal)
|
|
1136
|
+
.filter(Boolean)
|
|
1137
|
+
.filter((goal) => {
|
|
1138
|
+
const key = `${goal.goal_id || ''}:${goal.label}`;
|
|
1139
|
+
if (seen.has(key)) return false;
|
|
1140
|
+
seen.add(key);
|
|
1141
|
+
return true;
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function buildNextQuest(reviewTasks, touchedTasks, acceptedReceipts) {
|
|
1146
|
+
const pendingReview = reviewTasks
|
|
1147
|
+
.slice()
|
|
1148
|
+
.sort((a, b) => (latestTaskMs(b) || 0) - (latestTaskMs(a) || 0))[0];
|
|
1149
|
+
if (pendingReview) {
|
|
1150
|
+
return `Review ${taskRef(pendingReview)}: ${pendingReview.title}`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const activeTask = touchedTasks
|
|
1154
|
+
.filter(task => ['claimed', 'open', 'todo', 'do'].includes(String(task.status || '').toLowerCase()))
|
|
1155
|
+
.sort((a, b) => (latestTaskMs(b) || 0) - (latestTaskMs(a) || 0))[0];
|
|
1156
|
+
if (activeTask) {
|
|
1157
|
+
return `Move ${taskRef(activeTask)} to Review with proof.`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (!acceptedReceipts.length) {
|
|
1161
|
+
return 'Put one task into Review with proof, then accept it if the proof is real.';
|
|
1162
|
+
}
|
|
1163
|
+
return 'Pick the next highest-leverage task and move it to Review with proof.';
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function workspacePathCandidates(workspace) {
|
|
1167
|
+
const candidates = [workspace];
|
|
1168
|
+
try {
|
|
1169
|
+
candidates.push(fs.realpathSync(workspace));
|
|
1170
|
+
} catch (_) {
|
|
1171
|
+
// best-effort only
|
|
1172
|
+
}
|
|
1173
|
+
const pwd = process.env.PWD || '';
|
|
1174
|
+
if (pwd) {
|
|
1175
|
+
try {
|
|
1176
|
+
if (fs.existsSync(pwd) && fs.realpathSync(pwd) === fs.realpathSync(workspace)) candidates.push(pwd);
|
|
1177
|
+
} catch (_) {
|
|
1178
|
+
// best-effort only
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function readCodexGoalsForSession(args, workspace, sinceMs, untilMs) {
|
|
1185
|
+
const dbPath = path.resolve(expandHome(
|
|
1186
|
+
readFlag(args, '--codex-state', readFlag(args, '--state', process.env.CODEX_STATE_DB || CODEX_STATE_FILE))
|
|
1187
|
+
));
|
|
1188
|
+
const threadId = readFlag(args, '--thread', process.env.CODEX_THREAD_ID || '');
|
|
1189
|
+
const clauses = [];
|
|
1190
|
+
if (threadId) clauses.push(`tg.thread_id = ${sqlString(threadId)}`);
|
|
1191
|
+
const workspaceCandidates = workspacePathCandidates(workspace);
|
|
1192
|
+
if (workspaceCandidates.length) {
|
|
1193
|
+
clauses.push(`(t.cwd IN (${workspaceCandidates.map(sqlString).join(', ')}) AND tg.created_at_ms <= ${Number(untilMs)} AND (tg.updated_at_ms >= ${Number(sinceMs)} OR tg.status = 'active'))`);
|
|
1194
|
+
}
|
|
1195
|
+
if (!clauses.length) return [];
|
|
1196
|
+
|
|
1197
|
+
const rows = runSqliteJsonOptional(dbPath, `
|
|
1198
|
+
SELECT
|
|
1199
|
+
'codex' AS provider,
|
|
1200
|
+
tg.thread_id,
|
|
1201
|
+
tg.goal_id,
|
|
1202
|
+
tg.objective,
|
|
1203
|
+
tg.status,
|
|
1204
|
+
tg.token_budget,
|
|
1205
|
+
tg.tokens_used,
|
|
1206
|
+
tg.time_used_seconds,
|
|
1207
|
+
tg.created_at_ms,
|
|
1208
|
+
tg.updated_at_ms,
|
|
1209
|
+
t.cwd AS thread_cwd,
|
|
1210
|
+
t.title AS thread_title
|
|
1211
|
+
FROM thread_goals tg
|
|
1212
|
+
LEFT JOIN threads t ON t.id = tg.thread_id
|
|
1213
|
+
WHERE ${clauses.join(' OR ')}
|
|
1214
|
+
ORDER BY tg.updated_at_ms DESC
|
|
1215
|
+
LIMIT 25
|
|
1216
|
+
`);
|
|
1217
|
+
|
|
1218
|
+
return rows.map(row => ({
|
|
1219
|
+
...row,
|
|
1220
|
+
provider: 'codex',
|
|
1221
|
+
}));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function buildCareerXpSessionCapsule(args = []) {
|
|
1225
|
+
const workspace = path.resolve(readFlag(args, '--workspace', process.cwd()));
|
|
1226
|
+
const sinceInput = readFlag(args, '--since', 'today');
|
|
1227
|
+
const untilInput = readFlag(args, '--until', null);
|
|
1228
|
+
const since = parseSessionBoundary(sinceInput, startOfToday());
|
|
1229
|
+
const until = parseSessionBoundary(untilInput, new Date());
|
|
1230
|
+
const sinceMs = since.getTime();
|
|
1231
|
+
const untilMs = until.getTime();
|
|
1232
|
+
if (sinceMs > untilMs) {
|
|
1233
|
+
throw new Error('--since must be before --until');
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const writeEnabled = !hasFlag(args, '--no-write') && !hasFlag(args, '--dry-run');
|
|
1237
|
+
const projectionState = collectLocalXpProjectionState(['--workspace', workspace], { write: writeEnabled });
|
|
1238
|
+
const projection = projectionState.projection;
|
|
1239
|
+
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
1240
|
+
const taskProjectionPath = path.join(workspace, TASK_PROJECTION_FILE);
|
|
1241
|
+
const sessionDate = localDateKey(since) || since.toISOString().slice(0, 10);
|
|
1242
|
+
const sessionPath = path.join(workspace, CAREER_XP_SESSIONS_DIR, `${sessionDate}.json`);
|
|
1243
|
+
const warnings = [];
|
|
1244
|
+
if (projection.integrity?.cursor?.skipped_partial_tail) {
|
|
1245
|
+
warnings.push('task_episodes_partial_tail_waiting');
|
|
1246
|
+
}
|
|
1247
|
+
const taskProjection = readTaskProjectionState(workspace);
|
|
1248
|
+
if (taskProjection.warning) warnings.push(taskProjection.warning);
|
|
1249
|
+
const tasks = taskProjection.tasks;
|
|
1250
|
+
const touchedTasks = tasks
|
|
1251
|
+
.filter(task => taskTouchedInWindow(task, sinceMs, untilMs))
|
|
1252
|
+
.sort((a, b) => (latestTaskMs(b) || 0) - (latestTaskMs(a) || 0));
|
|
1253
|
+
const reviewTasks = touchedTasks.filter((task) => {
|
|
1254
|
+
const status = String(task.status || '').toLowerCase();
|
|
1255
|
+
return status === 'review' || taskApprovalStatus(task) === 'pending';
|
|
1256
|
+
});
|
|
1257
|
+
const doneTasks = touchedTasks.filter(task => String(task.status || '').toLowerCase() === 'done');
|
|
1258
|
+
const receipts = projection.integrity_status === 'verified' ? projectionState.receipts : [];
|
|
1259
|
+
const acceptedReceipts = receipts
|
|
1260
|
+
.filter(receipt => receipt?.outcome === 'accepted' && asNumber(receipt.xp) > 0 && inWindow(receipt.accepted_at, sinceMs, untilMs))
|
|
1261
|
+
.sort((a, b) => (timestampMs(b.accepted_at) || 0) - (timestampMs(a.accepted_at) || 0));
|
|
1262
|
+
const acceptedTasks = acceptedReceipts.map((receipt) => {
|
|
1263
|
+
const task = touchedTasks.find(item => receiptMatchesTask(receipt, item));
|
|
1264
|
+
return compactReceipt(receipt, task);
|
|
1265
|
+
});
|
|
1266
|
+
const windowXp = acceptedReceipts.reduce((sum, receipt) => sum + asNumber(receipt.xp), 0);
|
|
1267
|
+
const afterTotalXp = asNumber(projection.total_xp);
|
|
1268
|
+
const missionFlag = readFlag(args, '--mission', null);
|
|
1269
|
+
const episodeGoals = readJsonl(path.join(workspace, TASK_EPISODES_FILE), { allowPartialTail: true })
|
|
1270
|
+
.filter(episode => inWindow(episode?.created_at, sinceMs, untilMs))
|
|
1271
|
+
.map(episode => episode.goal);
|
|
1272
|
+
const codexGoals = readCodexGoalsForSession(args, workspace, sinceMs, untilMs);
|
|
1273
|
+
const goals = uniqueGoals([
|
|
1274
|
+
...codexGoals,
|
|
1275
|
+
...acceptedReceipts.map(receipt => receipt.goal),
|
|
1276
|
+
...episodeGoals,
|
|
1277
|
+
]);
|
|
1278
|
+
const latestAccepted = acceptedReceipts[0] || null;
|
|
1279
|
+
|
|
1280
|
+
const capsule = {
|
|
1281
|
+
schema: 'atris.career_xp_session_capsule.v1',
|
|
1282
|
+
generated_at: new Date().toISOString(),
|
|
1283
|
+
workspace_root: workspace,
|
|
1284
|
+
window: {
|
|
1285
|
+
label: String(sinceInput || 'today'),
|
|
1286
|
+
since: since.toISOString(),
|
|
1287
|
+
until: until.toISOString(),
|
|
1288
|
+
since_ms: sinceMs,
|
|
1289
|
+
until_ms: untilMs,
|
|
1290
|
+
},
|
|
1291
|
+
mission: {
|
|
1292
|
+
label: missionFlag || goals[0]?.label || touchedTasks.find(task => task.tag === 'career-xp')?.title || 'Capture proof-backed work',
|
|
1293
|
+
source: missionFlag ? 'flag' : goals[0] ? 'goal' : 'tasks',
|
|
1294
|
+
},
|
|
1295
|
+
xp: {
|
|
1296
|
+
before_agent_xp: Math.max(0, afterTotalXp - windowXp),
|
|
1297
|
+
after_agent_xp: afterTotalXp,
|
|
1298
|
+
delta_agent_xp: windowXp,
|
|
1299
|
+
today_agent_xp: asNumber(projection.today_xp),
|
|
1300
|
+
before_total_xp: Math.max(0, afterTotalXp - windowXp),
|
|
1301
|
+
after_total_xp: afterTotalXp,
|
|
1302
|
+
delta_xp: windowXp,
|
|
1303
|
+
today_xp: asNumber(projection.today_xp),
|
|
1304
|
+
level: asNumber(projection.level, 1),
|
|
1305
|
+
next_level_progress: projection.next_level_progress || null,
|
|
1306
|
+
integrity_status: projection.integrity_status || 'unknown',
|
|
1307
|
+
},
|
|
1308
|
+
tasks: {
|
|
1309
|
+
touched_count: touchedTasks.length,
|
|
1310
|
+
review_count: reviewTasks.length,
|
|
1311
|
+
accepted_count: acceptedTasks.length,
|
|
1312
|
+
done_count: doneTasks.length,
|
|
1313
|
+
touched: touchedTasks.map(compactTask),
|
|
1314
|
+
review: reviewTasks.map(compactTask),
|
|
1315
|
+
accepted: acceptedTasks,
|
|
1316
|
+
},
|
|
1317
|
+
goals: {
|
|
1318
|
+
count: goals.length,
|
|
1319
|
+
touched: goals,
|
|
1320
|
+
},
|
|
1321
|
+
proof: {
|
|
1322
|
+
receipts_count: acceptedReceipts.length,
|
|
1323
|
+
latest_accepted: latestAccepted ? compactReceipt(
|
|
1324
|
+
latestAccepted,
|
|
1325
|
+
touchedTasks.find(task => receiptMatchesTask(latestAccepted, task)),
|
|
1326
|
+
{ fullProof: true }
|
|
1327
|
+
) : null,
|
|
1328
|
+
accepted: acceptedTasks,
|
|
1329
|
+
},
|
|
1330
|
+
next_quest: buildNextQuest(reviewTasks, touchedTasks, acceptedReceipts),
|
|
1331
|
+
files: {
|
|
1332
|
+
projection_path: path.join(workspace, CAREER_XP_PROJECTION_FILE),
|
|
1333
|
+
receipts_path: receiptsPath,
|
|
1334
|
+
task_projection_path: fs.existsSync(taskProjectionPath) ? taskProjectionPath : null,
|
|
1335
|
+
session_path: writeEnabled ? sessionPath : null,
|
|
1336
|
+
},
|
|
1337
|
+
written: writeEnabled,
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
if (warnings.length) {
|
|
1341
|
+
capsule.warnings = warnings;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (writeEnabled) {
|
|
1345
|
+
writeJson(sessionPath, capsule);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return capsule;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function normalizeLocalScore(score, workspace) {
|
|
1352
|
+
const card = score.profile_card || score.player_card || {};
|
|
1353
|
+
const integrity = score.integrity || {};
|
|
1354
|
+
const careerXp = asNumber(card.agent_xp ?? card.career_xp);
|
|
1355
|
+
const leaderboardEligible = Boolean(
|
|
1356
|
+
card.leaderboard_eligible ?? integrity.leaderboard_eligible
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
metric_label: AGENT_XP_LABEL,
|
|
1361
|
+
agent_xp: careerXp,
|
|
1362
|
+
career_xp: careerXp,
|
|
1363
|
+
level: levelFromXp(careerXp),
|
|
1364
|
+
operator: score.operator || null,
|
|
1365
|
+
leaderboard_eligible: leaderboardEligible,
|
|
1366
|
+
source: 'local_contribution_score',
|
|
1367
|
+
workspace_root: score.workspace_root || workspace,
|
|
1368
|
+
current_form_by_arena: {
|
|
1369
|
+
local_workspace: {
|
|
1370
|
+
ovr: asNumber(card.ovr || card.current_form),
|
|
1371
|
+
current_form: asNumber(card.current_form || card.ovr),
|
|
1372
|
+
visible_stats: Array.isArray(card.visible_stats) ? card.visible_stats : [],
|
|
1373
|
+
leaderboard_eligible: leaderboardEligible,
|
|
1374
|
+
integrity_status: score.label || integrity.status || 'unknown',
|
|
1375
|
+
},
|
|
1376
|
+
},
|
|
1377
|
+
contribution_graph: score.contribution_graph || {},
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function renderContributionGraph(graph) {
|
|
1382
|
+
if (!graph || !Array.isArray(graph.days)) return;
|
|
1383
|
+
console.log('');
|
|
1384
|
+
console.log(`Last ${formatNumber(graph.window_days || DEFAULT_GRAPH_DAYS)} days: ${formatNumber(graph.total_xp)} ${AGENT_XP_LABEL} across ${formatNumber(graph.active_days)} active days`);
|
|
1385
|
+
for (const row of buildContributionRows(graph.days)) {
|
|
1386
|
+
console.log(row);
|
|
1387
|
+
}
|
|
1388
|
+
console.log('Legend: blank none | . started | : solid | * heavy | # breakout');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function loadLocalPayload(args) {
|
|
1392
|
+
const workspace = path.resolve(readFlag(args, '--workspace', process.cwd()));
|
|
1393
|
+
const operator = readFlag(
|
|
1394
|
+
args,
|
|
1395
|
+
'--operator',
|
|
1396
|
+
process.env.ATRIS_OPERATOR || process.env.USER || os.userInfo().username
|
|
1397
|
+
);
|
|
1398
|
+
const script = path.join(workspace, 'scripts', 'contribution_score.py');
|
|
1399
|
+
|
|
1400
|
+
if (!fs.existsSync(script)) {
|
|
1401
|
+
throw new Error(`No local contribution scorer found at ${path.relative(process.cwd(), script)}`);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const result = spawnSync(
|
|
1405
|
+
process.env.PYTHON || 'python3',
|
|
1406
|
+
[script, '--workspace', workspace, '--operator', operator, '--json'],
|
|
1407
|
+
{ cwd: workspace, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 }
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
if (result.error) {
|
|
1411
|
+
throw result.error;
|
|
1412
|
+
}
|
|
1413
|
+
if (result.status !== 0) {
|
|
1414
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
1415
|
+
throw new Error(detail || `Local scorer exited ${result.status}`);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return normalizeLocalScore(JSON.parse(result.stdout), workspace);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function render(payload) {
|
|
1422
|
+
if (payload.schema === 'atris.career_xp_session_capsule.v1') {
|
|
1423
|
+
const xp = payload.xp || {};
|
|
1424
|
+
const tasks = payload.tasks || {};
|
|
1425
|
+
console.log(`${AGENT_XP_LABEL} Session ${payload.window?.label || 'window'}`);
|
|
1426
|
+
console.log(`${AGENT_XP_LABEL} earned ${formatNumber(xp.delta_agent_xp ?? xp.delta_xp)} | Total ${formatNumber(xp.after_agent_xp ?? xp.after_total_xp)} | Today ${formatNumber(xp.today_agent_xp ?? xp.today_xp)} | Career Level ${formatNumber(xp.level || 1)}`);
|
|
1427
|
+
console.log(`Tasks ${formatNumber(tasks.touched_count)} touched | ${formatNumber(tasks.review_count)} in Review | ${formatNumber(tasks.accepted_count)} accepted`);
|
|
1428
|
+
if (payload.proof?.latest_accepted) {
|
|
1429
|
+
const proof = payload.proof.latest_accepted;
|
|
1430
|
+
console.log(`Latest proof ${proof.task_id || proof.title || 'accepted proof'}: ${textExcerpt(proof.proof, 260)}`);
|
|
1431
|
+
} else {
|
|
1432
|
+
console.log('Latest proof: none accepted in this session');
|
|
1433
|
+
}
|
|
1434
|
+
console.log(`Mission: ${payload.mission?.label || 'Capture proof-backed work'}`);
|
|
1435
|
+
console.log(`Next quest: ${payload.next_quest || 'Pick the next proof-backed task.'}`);
|
|
1436
|
+
if (payload.files?.session_path) console.log(`Capsule: ${payload.files.session_path}`);
|
|
1437
|
+
console.log(`Integrity: ${xp.integrity_status || 'unknown'}`);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (payload.schema === 'atris.career_xp_profile.v1') {
|
|
1442
|
+
const progress = payload.next_level_progress || {};
|
|
1443
|
+
console.log(`${AGENT_XP_LABEL} Card`);
|
|
1444
|
+
console.log(`${AGENT_XP_LABEL} ${formatNumber(payload.total_agent_xp ?? payload.total_xp)} | Today ${formatNumber(payload.today_agent_xp ?? payload.today_xp)} | Career Level ${formatNumber(payload.level || 1)}`);
|
|
1445
|
+
console.log(`Next career level ${formatNumber(progress.current_xp)}/${formatNumber(progress.required_xp)} ${AGENT_XP_LABEL} (${formatNumber(progress.percent)}%) | ${formatNumber(progress.remaining_xp)} to go`);
|
|
1446
|
+
console.log(`Workspaces ${formatNumber(payload.verified_workspace_count)}/${formatNumber(payload.workspace_count)} verified`);
|
|
1447
|
+
renderContributionGraph(payload.contribution_graph);
|
|
1448
|
+
console.log('');
|
|
1449
|
+
for (const workspace of payload.workspaces || []) {
|
|
1450
|
+
const marker = workspace.included ? 'included' : 'excluded';
|
|
1451
|
+
console.log(`- ${workspace.name}: ${formatNumber(workspace.total_xp)} ${AGENT_XP_LABEL} | today ${formatNumber(workspace.today_xp)} | ${workspace.integrity_status} | ${marker}`);
|
|
1452
|
+
}
|
|
1453
|
+
if (payload.latest_accepted_proof) {
|
|
1454
|
+
const proof = payload.latest_accepted_proof;
|
|
1455
|
+
console.log(`Latest proof ${proof.workspace_name || 'workspace'} / ${proof.label || proof.title || 'Accepted proof'}: ${proof.proof}`);
|
|
1456
|
+
} else {
|
|
1457
|
+
console.log('Latest proof: none accepted yet');
|
|
1458
|
+
}
|
|
1459
|
+
const integrity = payload.integrity || {};
|
|
1460
|
+
console.log(`Integrity: ${integrity.status || 'unknown'} (${integrity.local_trust || 'local'})`);
|
|
1461
|
+
for (const warning of integrity.warnings || []) {
|
|
1462
|
+
console.log(`Warning: ${warning.workspace_root} ${warning.reason}`);
|
|
1463
|
+
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (payload.schema === 'atris.career_xp_projection.v1') {
|
|
1468
|
+
const progress = payload.next_level_progress || {};
|
|
1469
|
+
console.log(`${AGENT_XP_LABEL} Card`);
|
|
1470
|
+
console.log(`${AGENT_XP_LABEL} ${formatNumber(payload.total_agent_xp ?? payload.total_xp)} | Today ${formatNumber(payload.today_agent_xp ?? payload.today_xp)} | Career Level ${formatNumber(payload.level || 1)}`);
|
|
1471
|
+
console.log(`Next career level ${formatNumber(progress.current_xp)}/${formatNumber(progress.required_xp)} ${AGENT_XP_LABEL} (${formatNumber(progress.percent)}%) | ${formatNumber(progress.remaining_xp)} to go`);
|
|
1472
|
+
renderContributionGraph(payload.contribution_graph);
|
|
1473
|
+
console.log('');
|
|
1474
|
+
if (payload.latest_accepted_proof) {
|
|
1475
|
+
const proof = payload.latest_accepted_proof;
|
|
1476
|
+
console.log(`Latest proof ${proof.label || proof.title || 'Accepted proof'}: ${proof.proof}`);
|
|
1477
|
+
} else {
|
|
1478
|
+
console.log('Latest proof: none accepted yet');
|
|
1479
|
+
}
|
|
1480
|
+
console.log(`Ledger: ${payload.ledger?.projection_path || CAREER_XP_PROJECTION_FILE}`);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const graph = payload.contribution_graph || {};
|
|
1485
|
+
const form = currentForm(payload);
|
|
1486
|
+
|
|
1487
|
+
console.log(`${AGENT_XP_LABEL} ${formatNumber(payload.agent_xp ?? payload.career_xp)} | Career Level ${formatNumber(payload.level || 1)}`);
|
|
1488
|
+
if (form) {
|
|
1489
|
+
const stats = form.visibleStats.length ? ` | ${form.visibleStats.join(', ')}` : '';
|
|
1490
|
+
console.log(`Current form ${form.ovr}/99 | ${form.integrityStatus}${stats}`);
|
|
1491
|
+
}
|
|
1492
|
+
console.log(`Last ${formatNumber(graph.window_days || 365)} days: ${formatNumber(graph.total_xp)} ${AGENT_XP_LABEL} across ${formatNumber(graph.active_days)} active days`);
|
|
1493
|
+
console.log('');
|
|
1494
|
+
for (const row of buildContributionRows(graph.days)) {
|
|
1495
|
+
console.log(row);
|
|
1496
|
+
}
|
|
1497
|
+
console.log('');
|
|
1498
|
+
console.log('Legend: blank none | . started | : solid | * heavy | # breakout');
|
|
1499
|
+
if (payload.leaderboard_eligible === false) {
|
|
1500
|
+
console.log('Leaderboard: integrity review needed before public ranking.');
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function xpCommand(...args) {
|
|
1505
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
1506
|
+
showHelp();
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const subcommand = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
|
1511
|
+
if (subcommand === 'session') {
|
|
1512
|
+
const commandArgs = args.slice(1);
|
|
1513
|
+
let payload;
|
|
1514
|
+
try {
|
|
1515
|
+
payload = buildCareerXpSessionCapsule(commandArgs);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
console.error(`Failed to build local XP session: ${error.message}`);
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
if (args.includes('--json')) {
|
|
1521
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
render(payload);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (subcommand === 'collect' || subcommand === 'status' || subcommand === 'card') {
|
|
1529
|
+
const commandArgs = args.slice(1);
|
|
1530
|
+
let payload;
|
|
1531
|
+
try {
|
|
1532
|
+
const explicitLocal = hasFlag(commandArgs, '--local')
|
|
1533
|
+
|| hasFlag(commandArgs, '--workspace')
|
|
1534
|
+
|| hasFlag(commandArgs, '--operator');
|
|
1535
|
+
const accountStatus = (subcommand === 'status' || subcommand === 'card') && !explicitLocal;
|
|
1536
|
+
payload = hasFlag(commandArgs, '--all') || accountStatus
|
|
1537
|
+
? collectAllLocalXpProjection(commandArgs)
|
|
1538
|
+
: collectLocalXpProjection(commandArgs);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
console.error(`Failed to collect local XP: ${error.message}`);
|
|
1541
|
+
process.exit(1);
|
|
1542
|
+
}
|
|
1543
|
+
if (args.includes('--json')) {
|
|
1544
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
render(payload);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (subcommand) {
|
|
1552
|
+
console.error(`Unknown xp subcommand: ${subcommand}`);
|
|
1553
|
+
showHelp();
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const jsonMode = args.includes('--json');
|
|
1558
|
+
const localMode = hasFlag(args, '--local') || hasFlag(args, '--workspace') || hasFlag(args, '--operator');
|
|
1559
|
+
if (localMode) {
|
|
1560
|
+
let payload;
|
|
1561
|
+
try {
|
|
1562
|
+
payload = collectLocalXpProjection(args);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
console.error(`Failed to collect local XP: ${error.message}`);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
if (jsonMode) {
|
|
1568
|
+
console.log(JSON.stringify(payload));
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
render(payload);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const ensured = await ensureValidCredentials(apiRequestJson);
|
|
1576
|
+
if (ensured.error) {
|
|
1577
|
+
console.error(`Not logged in. Run: atris login, or use --local inside a workspace${ensured.detail ? ` (${ensured.detail})` : ''}`);
|
|
1578
|
+
process.exit(1);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const result = await apiRequestJson('/profile/contribution-graph', {
|
|
1582
|
+
method: 'GET',
|
|
1583
|
+
token: ensured.credentials.token,
|
|
1584
|
+
});
|
|
1585
|
+
if (!result.ok) {
|
|
1586
|
+
console.error(`Failed to load XP graph: ${result.error || result.status}`);
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
if (jsonMode) {
|
|
1591
|
+
console.log(JSON.stringify(result.data || {}));
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
render(result.data || {});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
module.exports = {
|
|
1599
|
+
xpCommand,
|
|
1600
|
+
buildContributionRows,
|
|
1601
|
+
buildCareerXpProjection,
|
|
1602
|
+
buildAllCareerXpProjection,
|
|
1603
|
+
buildCareerXpSessionCapsule,
|
|
1604
|
+
collectAllLocalXpProjection,
|
|
1605
|
+
collectLocalXpProjection,
|
|
1606
|
+
receiptFromTaskEpisode,
|
|
1607
|
+
render,
|
|
1608
|
+
};
|