atris 3.1.0 → 3.5.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/GETTING_STARTED.md +65 -131
- package/README.md +29 -4
- package/atris/GETTING_STARTED.md +65 -131
- package/atris/PERSONA.md +5 -1
- package/atris/atris.md +122 -153
- package/atris/skills/aeo/SKILL.md +117 -0
- package/atris/skills/atris/SKILL.md +49 -25
- package/atris/skills/create-member/SKILL.md +29 -9
- package/atris/skills/endgame/SKILL.md +9 -0
- package/atris/skills/improve/SKILL.md +2 -2
- package/atris/skills/research-search/SKILL.md +167 -0
- package/atris/skills/research-search/arxiv_search.py +157 -0
- package/atris/skills/research-search/program.md +48 -0
- package/atris/skills/research-search/results.tsv +6 -0
- package/atris/skills/research-search/scholar_search.py +154 -0
- package/atris/skills/tidy/SKILL.md +36 -21
- package/atris/team/_template/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +35 -1
- package/atris.md +118 -178
- package/bin/atris.js +37 -6
- package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
- package/cli/atris_code.py +889 -0
- package/cli/runtime_guard.py +693 -0
- package/commands/align.js +15 -0
- package/commands/app.js +316 -0
- package/commands/autopilot.js +948 -42
- package/commands/business.js +691 -11
- package/commands/computer.js +1979 -43
- package/commands/context-sync.js +5 -0
- package/commands/experiments.js +1 -1
- package/commands/lifecycle.js +12 -0
- package/commands/plugin.js +24 -0
- package/commands/pull.js +40 -1
- package/commands/push.js +44 -0
- package/commands/release.js +183 -0
- package/commands/research.js +52 -0
- package/commands/serve.js +1 -0
- package/commands/sync.js +372 -87
- package/commands/verify.js +53 -4
- package/commands/wiki.js +71 -26
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/reward-config.js +24 -0
- package/lib/scorecard.js +58 -6
- package/lib/sync-telemetry.js +59 -0
- package/lib/todo.js +6 -0
- package/lib/wiki.js +235 -60
- package/package.json +4 -2
- package/utils/api.js +19 -0
- package/utils/auth.js +25 -1
- package/utils/config.js +24 -0
- package/utils/update-check.js +16 -0
package/commands/wiki.js
CHANGED
|
@@ -5,11 +5,17 @@ const { apiRequestJson } = require('../utils/api');
|
|
|
5
5
|
const { loadBusinesses, saveBusinesses } = require('./business');
|
|
6
6
|
const {
|
|
7
7
|
WIKI_ROOT,
|
|
8
|
+
PRIVATE_WIKI_ROOT,
|
|
9
|
+
getWikiRoot,
|
|
8
10
|
ensureWikiScaffold,
|
|
11
|
+
ensureContextScaffold,
|
|
9
12
|
findLocalWikiDir,
|
|
13
|
+
stageWikiIngest,
|
|
10
14
|
buildIngestPrompt,
|
|
11
15
|
buildQueryPrompt,
|
|
12
16
|
buildLintPrompt,
|
|
17
|
+
writeWikiStatus,
|
|
18
|
+
appendWikiLog,
|
|
13
19
|
} = require('../lib/wiki');
|
|
14
20
|
|
|
15
21
|
function autoDetectSlug() {
|
|
@@ -31,9 +37,14 @@ function parseCloudArgs(args) {
|
|
|
31
37
|
|
|
32
38
|
function parseModeArgs(args) {
|
|
33
39
|
const cloud = args.includes('--cloud');
|
|
40
|
+
const privateMode = args.includes('--private');
|
|
41
|
+
if (cloud && privateMode) {
|
|
42
|
+
console.error('Use either --cloud or --private, not both.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
34
45
|
return {
|
|
35
|
-
mode: cloud ? 'cloud' : 'local',
|
|
36
|
-
args: args.filter((arg) => arg !== '--cloud' && arg !== '--local'),
|
|
46
|
+
mode: cloud ? 'cloud' : (privateMode ? 'private' : 'local'),
|
|
47
|
+
args: args.filter((arg) => arg !== '--cloud' && arg !== '--local' && arg !== '--private'),
|
|
37
48
|
};
|
|
38
49
|
}
|
|
39
50
|
|
|
@@ -142,10 +153,10 @@ async function runChat(business, prompt, token) {
|
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
|
|
145
|
-
function printLocalPrompt(title, prompt, details = []) {
|
|
156
|
+
function printLocalPrompt(title, prompt, wikiRoot, details = []) {
|
|
146
157
|
console.log('');
|
|
147
158
|
console.log(title);
|
|
148
|
-
console.log(`Target: ${
|
|
159
|
+
console.log(`Target: ${wikiRoot}`);
|
|
149
160
|
details.forEach((detail) => console.log(detail));
|
|
150
161
|
console.log('');
|
|
151
162
|
console.log('Prompt for the current coding agent:');
|
|
@@ -160,11 +171,33 @@ async function wikiIngest(mode, slug, sourceValue) {
|
|
|
160
171
|
process.exit(1);
|
|
161
172
|
}
|
|
162
173
|
|
|
163
|
-
if (mode === 'local') {
|
|
164
|
-
const
|
|
165
|
-
|
|
174
|
+
if (mode === 'local' || mode === 'private') {
|
|
175
|
+
const wikiMode = mode === 'private' ? 'private' : 'public';
|
|
176
|
+
const wikiDir = ensureWikiScaffold(process.cwd(), wikiMode);
|
|
177
|
+
const contextDir = ensureContextScaffold(process.cwd(), wikiMode);
|
|
178
|
+
const staged = stageWikiIngest(process.cwd(), sourceValue, wikiMode);
|
|
179
|
+
writeWikiStatus(process.cwd(), {
|
|
180
|
+
health: `ingest staged from ${staged.packPath}`,
|
|
181
|
+
nextMove: `compile ${staged.promptSource} into ${getWikiRoot(wikiMode)}`,
|
|
182
|
+
}, wikiMode, { lastIngest: staged.manifest.ingested_at });
|
|
183
|
+
appendWikiLog(
|
|
184
|
+
process.cwd(),
|
|
185
|
+
`${staged.manifest.entries.length} source item(s) staged from ${sourceValue}`,
|
|
186
|
+
[
|
|
187
|
+
`context ${contextDir}`,
|
|
188
|
+
`pack ${staged.packPath}`,
|
|
189
|
+
`manifest ${staged.manifestPath}`,
|
|
190
|
+
...staged.manifest.entries.map((entry) => `${entry.kind} ${entry.staged}`),
|
|
191
|
+
],
|
|
192
|
+
wikiMode,
|
|
193
|
+
'INGEST'
|
|
194
|
+
);
|
|
195
|
+
printLocalPrompt(mode === 'private' ? 'Private wiki ingest' : 'Local wiki ingest', buildIngestPrompt(staged.promptSource, wikiMode), getWikiRoot(wikiMode), [
|
|
166
196
|
`Wiki dir: ${wikiDir}`,
|
|
167
|
-
`
|
|
197
|
+
`Context dir: ${contextDir}`,
|
|
198
|
+
`Pack: ${staged.packPath}`,
|
|
199
|
+
`Manifest: ${staged.manifestPath}`,
|
|
200
|
+
`Sources: ${staged.promptSource}`,
|
|
168
201
|
]);
|
|
169
202
|
return;
|
|
170
203
|
}
|
|
@@ -182,13 +215,14 @@ async function wikiQuery(mode, slug, question) {
|
|
|
182
215
|
process.exit(1);
|
|
183
216
|
}
|
|
184
217
|
|
|
185
|
-
if (mode
|
|
186
|
-
const
|
|
218
|
+
if (mode !== 'cloud') {
|
|
219
|
+
const wikiMode = mode === 'private' ? 'private' : 'public';
|
|
220
|
+
const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
|
|
187
221
|
if (!wikiDir) {
|
|
188
|
-
console.error(
|
|
222
|
+
console.error(`No local wiki found at ${getWikiRoot(wikiMode)}. Run: atris wiki ingest${wikiMode === 'private' ? ' --private' : ''} <path>`);
|
|
189
223
|
process.exit(1);
|
|
190
224
|
}
|
|
191
|
-
printLocalPrompt('Local wiki query', buildQueryPrompt(question), [
|
|
225
|
+
printLocalPrompt(mode === 'private' ? 'Private wiki query' : 'Local wiki query', buildQueryPrompt(question, wikiMode), getWikiRoot(wikiMode), [
|
|
192
226
|
`Wiki dir: ${wikiDir}`,
|
|
193
227
|
`Question: ${question}`,
|
|
194
228
|
]);
|
|
@@ -201,13 +235,14 @@ async function wikiQuery(mode, slug, question) {
|
|
|
201
235
|
}
|
|
202
236
|
|
|
203
237
|
async function wikiLint(mode, slug) {
|
|
204
|
-
if (mode
|
|
205
|
-
const
|
|
238
|
+
if (mode !== 'cloud') {
|
|
239
|
+
const wikiMode = mode === 'private' ? 'private' : 'public';
|
|
240
|
+
const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
|
|
206
241
|
if (!wikiDir) {
|
|
207
|
-
console.error(
|
|
242
|
+
console.error(`No local wiki found at ${getWikiRoot(wikiMode)}. Run: atris wiki ingest${wikiMode === 'private' ? ' --private' : ''} <path>`);
|
|
208
243
|
process.exit(1);
|
|
209
244
|
}
|
|
210
|
-
printLocalPrompt('Local wiki lint', buildLintPrompt(), [`Wiki dir: ${wikiDir}`]);
|
|
245
|
+
printLocalPrompt(mode === 'private' ? 'Private wiki lint' : 'Local wiki lint', buildLintPrompt(wikiMode), getWikiRoot(wikiMode), [`Wiki dir: ${wikiDir}`]);
|
|
211
246
|
return;
|
|
212
247
|
}
|
|
213
248
|
|
|
@@ -217,15 +252,16 @@ async function wikiLint(mode, slug) {
|
|
|
217
252
|
await runChat(business, buildLintPrompt(), creds.token);
|
|
218
253
|
}
|
|
219
254
|
|
|
220
|
-
function wikiSearch(slug, query) {
|
|
255
|
+
function wikiSearch(mode, slug, query) {
|
|
221
256
|
if (!query) {
|
|
222
257
|
console.error('Usage: atris wiki search [business] <term>');
|
|
223
258
|
process.exit(1);
|
|
224
259
|
}
|
|
225
260
|
|
|
226
|
-
const
|
|
261
|
+
const wikiMode = mode === 'private' ? 'private' : 'public';
|
|
262
|
+
const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
|
|
227
263
|
if (!wikiDir) {
|
|
228
|
-
console.error(`No local wiki found
|
|
264
|
+
console.error(`No local wiki found at ${getWikiRoot(wikiMode)}.`);
|
|
229
265
|
process.exit(1);
|
|
230
266
|
}
|
|
231
267
|
|
|
@@ -250,10 +286,11 @@ function wikiSearch(slug, query) {
|
|
|
250
286
|
console.log('');
|
|
251
287
|
}
|
|
252
288
|
|
|
253
|
-
function wikiLog(slug, limit) {
|
|
254
|
-
const
|
|
289
|
+
function wikiLog(mode, slug, limit) {
|
|
290
|
+
const wikiMode = mode === 'private' ? 'private' : 'public';
|
|
291
|
+
const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
|
|
255
292
|
if (!wikiDir) {
|
|
256
|
-
console.error(`No local wiki found
|
|
293
|
+
console.error(`No local wiki found at ${getWikiRoot(wikiMode)}.`);
|
|
257
294
|
process.exit(1);
|
|
258
295
|
}
|
|
259
296
|
|
|
@@ -314,14 +351,21 @@ async function wikiCommand(subcommand, ...args) {
|
|
|
314
351
|
break;
|
|
315
352
|
}
|
|
316
353
|
case 'search': {
|
|
317
|
-
|
|
318
|
-
|
|
354
|
+
if (mode === 'private') {
|
|
355
|
+
wikiSearch(mode, null, cleanArgs.join(' '));
|
|
356
|
+
} else {
|
|
357
|
+
const [slug, query] = parseCloudArgs(cleanArgs);
|
|
358
|
+
wikiSearch(mode, slug, query);
|
|
359
|
+
}
|
|
319
360
|
break;
|
|
320
361
|
}
|
|
321
362
|
case 'log': {
|
|
322
363
|
let slug;
|
|
323
364
|
let limit;
|
|
324
|
-
if (
|
|
365
|
+
if (mode === 'private') {
|
|
366
|
+
slug = null;
|
|
367
|
+
limit = parseInt(cleanArgs[0], 10) || 20;
|
|
368
|
+
} else if (cleanArgs.length === 0) {
|
|
325
369
|
slug = autoDetectSlug();
|
|
326
370
|
limit = 20;
|
|
327
371
|
} else if (cleanArgs.length === 1) {
|
|
@@ -336,7 +380,7 @@ async function wikiCommand(subcommand, ...args) {
|
|
|
336
380
|
slug = cleanArgs[0];
|
|
337
381
|
limit = parseInt(cleanArgs[1], 10) || 20;
|
|
338
382
|
}
|
|
339
|
-
wikiLog(slug, limit);
|
|
383
|
+
wikiLog(mode, slug, limit);
|
|
340
384
|
break;
|
|
341
385
|
}
|
|
342
386
|
case 'loop': {
|
|
@@ -361,6 +405,7 @@ async function wikiCommand(subcommand, ...args) {
|
|
|
361
405
|
console.log('Flags:');
|
|
362
406
|
console.log(' --cloud Route ingest/query/lint to the cloud workspace');
|
|
363
407
|
console.log(' --local Be explicit about local mode');
|
|
408
|
+
console.log(` --private Use local private wiki at ${PRIVATE_WIKI_ROOT}/`);
|
|
364
409
|
console.log('');
|
|
365
410
|
console.log('Business is auto-detected from .atris/business.json for cloud mode if omitted.');
|
|
366
411
|
}
|
package/lib/file-ops.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Get the path components for a journal log file.
|
|
6
|
+
* @param {string} [dateStr] - Optional date string (defaults to today)
|
|
7
|
+
* @returns {Object} Object with logsDir, yearDir, logFile, dateFormatted
|
|
8
|
+
*/
|
|
5
9
|
function getLogPath(dateStr) {
|
|
6
10
|
const targetDir = path.join(process.cwd(), 'atris');
|
|
7
11
|
const date = dateStr ? new Date(dateStr) : new Date();
|
|
@@ -17,6 +21,9 @@ function getLogPath(dateStr) {
|
|
|
17
21
|
return { logsDir, yearDir, logFile, dateFormatted };
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the logs directory structure exists.
|
|
26
|
+
*/
|
|
20
27
|
function ensureLogDirectory() {
|
|
21
28
|
const { logsDir, yearDir } = getLogPath();
|
|
22
29
|
|
|
@@ -29,6 +36,11 @@ function ensureLogDirectory() {
|
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Create a new daily log file, carrying forward unfinished items from yesterday.
|
|
41
|
+
* @param {string} logFile - Path to the log file to create
|
|
42
|
+
* @param {string} dateFormatted - Date string in YYYY-MM-DD format
|
|
43
|
+
*/
|
|
32
44
|
function createLogFile(logFile, dateFormatted) {
|
|
33
45
|
let carryInProgress = '';
|
|
34
46
|
let carryBacklog = '';
|
package/lib/journal.js
CHANGED
|
@@ -4,6 +4,12 @@ const path = require('path');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Check if two timestamps are effectively the same (within 5ms).
|
|
9
|
+
* @param {string} a - First timestamp
|
|
10
|
+
* @param {string} b - Second timestamp
|
|
11
|
+
* @returns {boolean} True if timestamps are within 5ms of each other
|
|
12
|
+
*/
|
|
7
13
|
function isSameTimestamp(a, b) {
|
|
8
14
|
if (!a || !b) return false;
|
|
9
15
|
const ta = new Date(a).getTime();
|
|
@@ -12,6 +18,11 @@ function isSameTimestamp(a, b) {
|
|
|
12
18
|
return Math.abs(ta - tb) < 5;
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Compute a SHA-256 hash of content with normalized line endings.
|
|
23
|
+
* @param {string} content - Content to hash
|
|
24
|
+
* @returns {string|null} Hex hash or null if invalid input
|
|
25
|
+
*/
|
|
15
26
|
function computeContentHash(content) {
|
|
16
27
|
if (typeof content !== 'string') {
|
|
17
28
|
return null;
|
|
@@ -20,6 +31,11 @@ function computeContentHash(content) {
|
|
|
20
31
|
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Parse journal content into named sections (by ## headers).
|
|
36
|
+
* @param {string} content - Journal markdown content
|
|
37
|
+
* @returns {Object} Map of section name to content
|
|
38
|
+
*/
|
|
23
39
|
function parseJournalSections(content) {
|
|
24
40
|
const sections = {};
|
|
25
41
|
const lines = content.split('\n');
|
|
@@ -48,6 +64,13 @@ function parseJournalSections(content) {
|
|
|
48
64
|
return sections;
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Merge local and remote journal sections, detecting conflicts.
|
|
69
|
+
* @param {Object} localSections - Local section map
|
|
70
|
+
* @param {Object} remoteSections - Remote section map
|
|
71
|
+
* @param {string} knownRemoteHash - Hash of remote at last sync
|
|
72
|
+
* @returns {{merged: Object, conflicts: Array}} Merged sections and conflicts
|
|
73
|
+
*/
|
|
51
74
|
function mergeSections(localSections, remoteSections, knownRemoteHash) {
|
|
52
75
|
const merged = {};
|
|
53
76
|
const conflicts = [];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen reward constants for the RL loop.
|
|
3
|
+
*
|
|
4
|
+
* These live outside mutable repo state so the loop cannot edit its own
|
|
5
|
+
* judge. REWARD_CHECKSUM is the SHA-256 of JSON.stringify(REWARD_CONFIG)
|
|
6
|
+
* + computeTickReward.toString() at ship time — if the config values or
|
|
7
|
+
* function body change, verifyJudgeIntegrity() halts the next tick.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
const REWARD_CONFIG = Object.freeze({
|
|
13
|
+
REVIEW_CLEAN: 1,
|
|
14
|
+
VERIFY_PASS: 3,
|
|
15
|
+
NPM_TEST_BONUS: 2,
|
|
16
|
+
COMMIT_LANDED: 1,
|
|
17
|
+
HALT_PENALTY: -3,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// SHA-256 of JSON.stringify(REWARD_CONFIG) + computeTickReward.toString() at ship time.
|
|
21
|
+
// Regenerate: node -e "const c=require('./lib/reward-config');const h=require('crypto').createHash('sha256');h.update(JSON.stringify(c.REWARD_CONFIG));h.update(require('./commands/autopilot').computeTickReward.toString());console.log(h.digest('hex'))"
|
|
22
|
+
const REWARD_CHECKSUM = '5a84be0f7f392d6ef05337be0776f864852e94d6391da0b41486298555595a40';
|
|
23
|
+
|
|
24
|
+
module.exports = { REWARD_CONFIG, REWARD_CHECKSUM };
|
package/lib/scorecard.js
CHANGED
|
@@ -2,6 +2,33 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { parseTodo } = require('./todo');
|
|
4
4
|
|
|
5
|
+
const PRIVATE_MEMORY_ROOT = '.atris/presidio';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensure the private memory directory exists.
|
|
9
|
+
* @param {string} atrisDir - Path to the atris directory
|
|
10
|
+
* @returns {string} Path to the private memory directory
|
|
11
|
+
*/
|
|
12
|
+
function ensurePrivateMemoryDir(atrisDir) {
|
|
13
|
+
const privateDir = path.join(path.dirname(atrisDir), PRIVATE_MEMORY_ROOT);
|
|
14
|
+
fs.mkdirSync(privateDir, { recursive: true });
|
|
15
|
+
return privateDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the path to the scorecards file.
|
|
20
|
+
* @param {string} atrisDir - Path to the atris directory
|
|
21
|
+
* @returns {string} Path to scorecards.md
|
|
22
|
+
*/
|
|
23
|
+
function getScorecardsPath(atrisDir) {
|
|
24
|
+
return path.join(ensurePrivateMemoryDir(atrisDir), 'scorecards.md');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse a "picked at" timestamp from scorecard data.
|
|
29
|
+
* @param {string} value - Timestamp string in YYYY-MM-DD [HH:MM] format
|
|
30
|
+
* @returns {Date|null} Parsed date or null if invalid
|
|
31
|
+
*/
|
|
5
32
|
function parsePickedAt(value) {
|
|
6
33
|
if (!value) return null;
|
|
7
34
|
const match = String(value).trim().match(/^(\d{4}-\d{2}-\d{2})(?:\s+(\d{2}:\d{2}))?/);
|
|
@@ -12,6 +39,12 @@ function parsePickedAt(value) {
|
|
|
12
39
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
13
40
|
}
|
|
14
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Parse a tick timestamp combining date and time label.
|
|
44
|
+
* @param {string} dateStr - Date in YYYY-MM-DD format
|
|
45
|
+
* @param {string} timeLabel - Time label like "2:30 PM"
|
|
46
|
+
* @returns {Date|null} Parsed date or null if invalid
|
|
47
|
+
*/
|
|
15
48
|
function parseTickDate(dateStr, timeLabel) {
|
|
16
49
|
const match = String(timeLabel || '').trim().toLowerCase().match(/^(\d{1,2}):(\d{2})(?:\s*(am|pm))?$/);
|
|
17
50
|
if (!match) return null;
|
|
@@ -28,6 +61,13 @@ function parseTickDate(dateStr, timeLabel) {
|
|
|
28
61
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
29
62
|
}
|
|
30
63
|
|
|
64
|
+
/**
|
|
65
|
+
* List journal log files within a date range.
|
|
66
|
+
* @param {string} atrisDir - Path to the atris directory
|
|
67
|
+
* @param {Date} startDate - Start of date range
|
|
68
|
+
* @param {Date} [endDate] - End of date range (defaults to today)
|
|
69
|
+
* @returns {Array} Array of log file paths
|
|
70
|
+
*/
|
|
31
71
|
function listLogFiles(atrisDir, startDate, endDate = new Date()) {
|
|
32
72
|
const logsDir = path.join(atrisDir, 'logs');
|
|
33
73
|
if (!fs.existsSync(logsDir)) return [];
|
|
@@ -141,16 +181,20 @@ function buildScorecardData(atrisDir, { slug, pickedAt } = {}) {
|
|
|
141
181
|
const todo = parseTodo(todoPath);
|
|
142
182
|
const startAt = parsePickedAt(pickedAt) || new Date();
|
|
143
183
|
const rewardStats = collectRewardStats(atrisDir, pickedAt);
|
|
144
|
-
|
|
184
|
+
// Count shipped tasks from journal completions (tasks get deleted from TODO.md after completion)
|
|
185
|
+
const completedFromTodo = todo.completed.filter(t => t.tag === 'endgame').length;
|
|
145
186
|
const activeEndgame = todo.backlog.filter(t => t.tag === 'endgame').length
|
|
146
187
|
+ todo.inProgress.filter(t => t.tag === 'endgame').length;
|
|
188
|
+
// Fall back to reward tick count if TODO completions were already pruned
|
|
189
|
+
const shipped = completedFromTodo > 0 ? completedFromTodo : rewardStats.totalTicks - rewardStats.haltedTicks;
|
|
190
|
+
const attempted = shipped + activeEndgame + rewardStats.haltedTicks;
|
|
147
191
|
|
|
148
192
|
return {
|
|
149
193
|
slug,
|
|
150
194
|
startDate: startAt.toISOString().slice(0, 10),
|
|
151
195
|
endDate: new Date().toISOString().slice(0, 10),
|
|
152
|
-
tasksShipped:
|
|
153
|
-
tasksAttempted:
|
|
196
|
+
tasksShipped: Math.max(shipped, 0),
|
|
197
|
+
tasksAttempted: Math.max(attempted, shipped),
|
|
154
198
|
wallClockHours: Math.max(0, (Date.now() - startAt.getTime()) / (1000 * 60 * 60)),
|
|
155
199
|
haltRatio: rewardStats.totalTicks > 0 ? rewardStats.haltedTicks / rewardStats.totalTicks : 0,
|
|
156
200
|
totalReward: rewardStats.totalReward,
|
|
@@ -191,7 +235,13 @@ function writeScorecard(atrisDir, data) {
|
|
|
191
235
|
throw new Error('Scorecard: slug is required');
|
|
192
236
|
}
|
|
193
237
|
|
|
194
|
-
const scorecardsPath =
|
|
238
|
+
const scorecardsPath = getScorecardsPath(atrisDir);
|
|
239
|
+
|
|
240
|
+
// Dedupe guard: don't write the same slug twice
|
|
241
|
+
const existing = readScorecards(atrisDir);
|
|
242
|
+
if (existing.some(sc => sc.slug === slug)) {
|
|
243
|
+
return; // already written
|
|
244
|
+
}
|
|
195
245
|
|
|
196
246
|
// Ensure scorecards.md exists
|
|
197
247
|
if (!fs.existsSync(scorecardsPath)) {
|
|
@@ -244,14 +294,14 @@ function detectEndgameCompletion(atrisDir) {
|
|
|
244
294
|
* Parse scorecards.md and return array of scorecard objects.
|
|
245
295
|
*/
|
|
246
296
|
function readScorecards(atrisDir) {
|
|
247
|
-
const scorecardsPath =
|
|
297
|
+
const scorecardsPath = getScorecardsPath(atrisDir);
|
|
248
298
|
if (!fs.existsSync(scorecardsPath)) return [];
|
|
249
299
|
|
|
250
300
|
const content = fs.readFileSync(scorecardsPath, 'utf8');
|
|
251
301
|
const scorecards = [];
|
|
252
302
|
|
|
253
303
|
for (const line of content.split('\n')) {
|
|
254
|
-
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*—\s*shipped:\s*(\d+)\/(\d+)\s*—\s*wall-clock:\s*(.+?)\s*—\s*halt:\s*(\d+)%\s*—\s*reward:\s*(
|
|
304
|
+
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*—\s*shipped:\s*(\d+)\/(\d+)\s*—\s*wall-clock:\s*(.+?)\s*—\s*halt:\s*(\d+)%\s*—\s*reward:\s*(-?\d+)\s*—\s*lessons:\s*(\d+)$/);
|
|
255
305
|
if (!match) continue;
|
|
256
306
|
|
|
257
307
|
const [, endDate, slug, shipped, attempted, wallClockStr, haltPercent, reward, lessons] = match;
|
|
@@ -280,6 +330,8 @@ function readScorecards(atrisDir) {
|
|
|
280
330
|
}
|
|
281
331
|
|
|
282
332
|
module.exports = {
|
|
333
|
+
PRIVATE_MEMORY_ROOT,
|
|
334
|
+
getScorecardsPath,
|
|
283
335
|
buildScorecardData,
|
|
284
336
|
writeScorecard,
|
|
285
337
|
readScorecards,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Atris sync telemetry — emit one event per `atris push` / `atris pull`.
|
|
2
|
+
//
|
|
3
|
+
// Why awaited (not fire-and-forget): push.js / pull.js call process.exit(1)
|
|
4
|
+
// on most failure paths. A fire-and-forget POST gets killed mid-flight and
|
|
5
|
+
// the RL loop loses signals on the exact failures it most needs to learn.
|
|
6
|
+
//
|
|
7
|
+
// Best-effort: any error swallowed silently — telemetry never blocks UX.
|
|
8
|
+
// Hard 2s timeout so a flaky control plane can't slow real operations.
|
|
9
|
+
|
|
10
|
+
const { apiRequestJson } = require('../utils/api');
|
|
11
|
+
|
|
12
|
+
let _cachedVersion = null;
|
|
13
|
+
function cliVersion() {
|
|
14
|
+
if (_cachedVersion) return _cachedVersion;
|
|
15
|
+
try {
|
|
16
|
+
_cachedVersion = require('../package.json').version || 'unknown';
|
|
17
|
+
} catch {
|
|
18
|
+
_cachedVersion = 'unknown';
|
|
19
|
+
}
|
|
20
|
+
return _cachedVersion;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function emitSyncEvent(token, businessId, workspaceId, op, outcome, latencyMs, extras = {}) {
|
|
24
|
+
if (!token || !businessId || !workspaceId || !op || !outcome) return false;
|
|
25
|
+
const event = {
|
|
26
|
+
business_id: businessId,
|
|
27
|
+
workspace_id: workspaceId,
|
|
28
|
+
op,
|
|
29
|
+
outcome,
|
|
30
|
+
latency_ms: Math.max(0, Math.round(latencyMs || 0)),
|
|
31
|
+
bytes_transferred: extras.bytes_transferred || 0,
|
|
32
|
+
bytes_changed: extras.bytes_changed || 0,
|
|
33
|
+
files_pushed: extras.files_pushed || 0,
|
|
34
|
+
files_deleted: extras.files_deleted || 0,
|
|
35
|
+
files_unchanged: extras.files_unchanged || 0,
|
|
36
|
+
cli_version: cliVersion(),
|
|
37
|
+
};
|
|
38
|
+
if (extras.error_detail) event.error_detail = String(extras.error_detail).slice(0, 500);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await apiRequestJson('/atris-sync/telemetry', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
token,
|
|
44
|
+
body: event,
|
|
45
|
+
timeoutMs: 2000,
|
|
46
|
+
retries: 0,
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function startTimer() {
|
|
55
|
+
const t0 = Date.now();
|
|
56
|
+
return () => Date.now() - t0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { emitSyncEvent, startTimer };
|
package/lib/todo.js
CHANGED
|
@@ -17,6 +17,12 @@ function parseTodo(todoPath) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse a specific section from TODO.md content into task objects.
|
|
22
|
+
* @param {string} content - Full TODO.md content
|
|
23
|
+
* @param {string} sectionName - Section name to extract (e.g., 'Backlog')
|
|
24
|
+
* @returns {Array} Array of parsed task objects
|
|
25
|
+
*/
|
|
20
26
|
function parseSection(content, sectionName) {
|
|
21
27
|
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
28
|
const match = content.match(new RegExp(`##\\s+${escaped}\\n([\\s\\S]*?)(?=\\n##|$)`, 'i'));
|