atris 3.15.57 → 3.16.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.
Files changed (43) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +11 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +31 -30
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +197 -22
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/computer.js +0 -60
  18. package/commands/improve.js +501 -0
  19. package/commands/integrations.js +233 -71
  20. package/commands/lesson.js +44 -0
  21. package/commands/member.js +4498 -226
  22. package/commands/mission.js +302 -27
  23. package/commands/now.js +89 -1
  24. package/commands/radar.js +181 -56
  25. package/commands/soul.js +0 -4
  26. package/commands/task.js +5582 -517
  27. package/commands/terminal.js +14 -10
  28. package/commands/wiki.js +87 -1
  29. package/commands/workflow.js +288 -73
  30. package/commands/worktree.js +52 -15
  31. package/commands/xp.js +6 -65
  32. package/lib/auto-accept-certified.js +294 -0
  33. package/lib/file-ops.js +0 -184
  34. package/lib/member-alive.js +232 -0
  35. package/lib/policy-lessons.js +280 -0
  36. package/lib/receipt-evidence.js +64 -0
  37. package/lib/state-detection.js +34 -0
  38. package/lib/task-db.js +568 -16
  39. package/lib/task-proof.js +43 -0
  40. package/package.json +1 -1
  41. package/utils/auth.js +13 -4
  42. package/commands/research.js +0 -52
  43. package/lib/section-merge.js +0 -196
package/commands/brain.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
+ const { spawnSync } = require('child_process');
4
5
  const { refreshNowFile } = require('./now');
5
6
 
6
7
  const GENERATED_START = '<!-- ATRIS_BRAIN_COMPILE:START -->';
@@ -18,6 +19,39 @@ const OPTIONAL_LOAD_ORDER_FILES = [
18
19
  'atris/TODO.md',
19
20
  'atris/wiki/index.md',
20
21
  ];
22
+ const CORE_STATE_FILES = [
23
+ 'events.jsonl',
24
+ 'episodes.jsonl',
25
+ 'task_episodes.jsonl',
26
+ 'scorecards.jsonl',
27
+ 'agent_tasks.jsonl',
28
+ 'agent_mail.jsonl',
29
+ 'agent_inboxes.jsonl',
30
+ 'agents.jsonl',
31
+ 'approvals.jsonl',
32
+ ];
33
+ const LOOP_HEALTH_CHANNELS = [
34
+ { label: 'Task plane', files: ['task_events.jsonl', 'tasks.projection.json'] },
35
+ { label: 'Overnight RL', files: ['overnight_rl_self_heal.jsonl'] },
36
+ { label: 'Career XP', files: ['career_xp_receipts.jsonl', 'career_xp.projection.json', 'gm_xp.projection.json'] },
37
+ { label: 'Master loop', files: ['master_loop_events.jsonl'] },
38
+ { label: 'Missions', files: ['mission_events.jsonl', 'missions.jsonl'] },
39
+ { label: 'Company YC', files: ['company_yc_wow_events.jsonl', 'company_yc_wow_latest.json'] },
40
+ { label: 'Codex goal', files: ['codex_goal.json'] },
41
+ { label: 'Pulse AGI', files: ['pulse_agi_loop_receipts.jsonl'] },
42
+ ];
43
+ const TIMESTAMP_KEYS = new Set([
44
+ 'at',
45
+ 'created_at',
46
+ 'date',
47
+ 'generated_at',
48
+ 'last_checked_at',
49
+ 'started_at',
50
+ 'synced_at',
51
+ 'timestamp',
52
+ 'ts',
53
+ 'updated_at',
54
+ ]);
21
55
 
22
56
  function parseArgs(args) {
23
57
  const options = {
@@ -142,6 +176,60 @@ function readJsonlStats(filePath) {
142
176
  };
143
177
  }
144
178
 
179
+ function latestTimestampFromValue(value, depth = 0) {
180
+ if (depth > 5 || value == null) return null;
181
+ if (Array.isArray(value)) {
182
+ return value
183
+ .map(item => latestTimestampFromValue(item, depth + 1))
184
+ .filter(Boolean)
185
+ .sort()
186
+ .pop() || null;
187
+ }
188
+ if (typeof value !== 'object') return null;
189
+
190
+ let latest = null;
191
+ for (const [key, child] of Object.entries(value)) {
192
+ if (TIMESTAMP_KEYS.has(key) && (typeof child === 'string' || typeof child === 'number')) {
193
+ const ts = String(child);
194
+ if (!latest || ts > latest) latest = ts;
195
+ continue;
196
+ }
197
+ const childTs = latestTimestampFromValue(child, depth + 1);
198
+ if (childTs && (!latest || childTs > latest)) latest = childTs;
199
+ }
200
+ return latest;
201
+ }
202
+
203
+ function readJsonStats(filePath) {
204
+ if (!fs.existsSync(filePath)) {
205
+ return { path: filePath, exists: false, rows: 0, validRows: 0, latestTs: null };
206
+ }
207
+ const parsed = readJson(filePath);
208
+ return {
209
+ path: filePath,
210
+ exists: true,
211
+ rows: 1,
212
+ validRows: parsed === null ? 0 : 1,
213
+ latestTs: latestTimestampFromValue(parsed),
214
+ };
215
+ }
216
+
217
+ function readStateFileStats(filePath) {
218
+ if (filePath.endsWith('.jsonl')) return readJsonlStats(filePath);
219
+ if (filePath.endsWith('.json')) return readJsonStats(filePath);
220
+ return { path: filePath, exists: fs.existsSync(filePath), rows: 0, validRows: 0, latestTs: null };
221
+ }
222
+
223
+ function collectStateFileStats(stateDir) {
224
+ const names = new Set(CORE_STATE_FILES);
225
+ if (fs.existsSync(stateDir)) {
226
+ for (const name of fs.readdirSync(stateDir).sort()) {
227
+ if (name.endsWith('.json') || name.endsWith('.jsonl')) names.add(name);
228
+ }
229
+ }
230
+ return Array.from(names).map(name => readStateFileStats(path.join(stateDir, name)));
231
+ }
232
+
145
233
  function readJsonlRows(filePath) {
146
234
  const text = readText(filePath);
147
235
  const rows = [];
@@ -156,6 +244,56 @@ function readJsonlRows(filePath) {
156
244
  return rows;
157
245
  }
158
246
 
247
+ function parseGitWorktrees(text) {
248
+ const out = [];
249
+ let current = {};
250
+ for (const raw of `${text || ''}\n`.split(/\r?\n/)) {
251
+ const line = raw.trim();
252
+ if (!line) {
253
+ if (current.worktree) out.push(path.resolve(current.worktree));
254
+ current = {};
255
+ continue;
256
+ }
257
+ const idx = line.indexOf(' ');
258
+ if (idx === -1) {
259
+ current[line] = true;
260
+ } else {
261
+ current[line.slice(0, idx)] = line.slice(idx + 1);
262
+ }
263
+ }
264
+ return out;
265
+ }
266
+
267
+ function primaryWorktreeRoot(root) {
268
+ const result = spawnSync('git', ['worktree', 'list', '--porcelain'], {
269
+ cwd: root,
270
+ encoding: 'utf8',
271
+ });
272
+ if (result.status !== 0) return null;
273
+ const [primary] = parseGitWorktrees(result.stdout);
274
+ return primary || null;
275
+ }
276
+
277
+ function stateRowCount(stateDir) {
278
+ return collectStateFileStats(stateDir)
279
+ .reduce((sum, item) => sum + item.rows, 0);
280
+ }
281
+
282
+ function resolveStateRoots(root) {
283
+ const primary = primaryWorktreeRoot(root);
284
+ if (!primary || path.resolve(primary) === path.resolve(root)) return [root];
285
+
286
+ const primaryStateDir = path.join(primary, '.atris', 'state');
287
+ if (!fs.existsSync(primaryStateDir)) return [root];
288
+
289
+ return stateRowCount(primaryStateDir) > 0 ? [root, primary] : [root];
290
+ }
291
+
292
+ function resolveStateRoot(root) {
293
+ const roots = resolveStateRoots(root);
294
+ return roots[roots.length - 1] || root;
295
+ }
296
+
159
297
  function countTodoItems(todoText) {
160
298
  const text = String(todoText || '');
161
299
  const hasRenderedSections = /^##\s+(Backlog|In Progress|Blocked|Completed)\s*$/m.test(text);
@@ -297,7 +435,9 @@ function isNextMoveScorecard(row) {
297
435
 
298
436
  function collectState(root) {
299
437
  const atrisDir = path.join(root, 'atris');
300
- const stateDir = path.join(root, '.atris', 'state');
438
+ const stateRoot = resolveStateRoot(root);
439
+ const stateRoots = resolveStateRoots(root);
440
+ const stateDirs = stateRoots.map(item => path.join(item, '.atris', 'state'));
301
441
  const business = readJson(path.join(root, '.atris', 'business.json')) || {};
302
442
  const todoText = readText(path.join(atrisDir, 'TODO.md'));
303
443
  const mapText = readText(path.join(atrisDir, 'MAP.md'));
@@ -305,21 +445,11 @@ function collectState(root) {
305
445
  const wikiStatus = readText(path.join(atrisDir, 'wiki', 'STATUS.md'));
306
446
  const status = readText(path.join(atrisDir, 'STATUS.md'));
307
447
 
308
- const stateFiles = [
309
- 'events.jsonl',
310
- 'episodes.jsonl',
311
- 'task_episodes.jsonl',
312
- 'scorecards.jsonl',
313
- 'agent_tasks.jsonl',
314
- 'agent_mail.jsonl',
315
- 'agent_inboxes.jsonl',
316
- 'agents.jsonl',
317
- 'approvals.jsonl',
318
- ].map(name => readJsonlStats(path.join(stateDir, name)));
448
+ const stateFiles = stateDirs.flatMap(stateDir => collectStateFileStats(stateDir));
319
449
 
320
450
  const totalRows = stateFiles.reduce((sum, item) => sum + item.rows, 0);
321
451
  const validRows = stateFiles.reduce((sum, item) => sum + item.validRows, 0);
322
- const latestScorecard = readJsonlRows(path.join(stateDir, 'scorecards.jsonl'))
452
+ const latestScorecard = stateDirs.flatMap(stateDir => readJsonlRows(path.join(stateDir, 'scorecards.jsonl')))
323
453
  .filter(isNextMoveScorecard)
324
454
  .sort((a, b) => scorecardTs(a).localeCompare(scorecardTs(b)))
325
455
  .pop() || null;
@@ -344,6 +474,9 @@ function collectState(root) {
344
474
  mapLineCount: mapText ? mapText.split('\n').length : 0,
345
475
  wikiPages: listMarkdown(root, 'atris/wiki', 20),
346
476
  stateFiles,
477
+ stateRoot,
478
+ stateRoots,
479
+ loopHealth: buildLoopHealth(stateFiles),
347
480
  totalRows,
348
481
  validRows,
349
482
  latestScorecard: latestScorecard ? {
@@ -368,14 +501,46 @@ function countStateRows(state, names) {
368
501
  .reduce((sum, item) => sum + item.rows, 0);
369
502
  }
370
503
 
504
+ function stateFilesForNames(stateFiles, names) {
505
+ const wanted = new Set(names);
506
+ return stateFiles.filter(item => wanted.has(path.basename(item.path)));
507
+ }
508
+
509
+ function buildLoopHealth(stateFiles) {
510
+ return LOOP_HEALTH_CHANNELS.map(channel => {
511
+ const files = stateFilesForNames(stateFiles, channel.files);
512
+ const rows = files.reduce((sum, item) => sum + item.rows, 0);
513
+ const validRows = files.reduce((sum, item) => sum + item.validRows, 0);
514
+ const latestTs = files
515
+ .map(item => item.latestTs)
516
+ .filter(Boolean)
517
+ .sort()
518
+ .pop() || null;
519
+ return {
520
+ label: channel.label,
521
+ files: channel.files,
522
+ rows,
523
+ validRows,
524
+ latestTs,
525
+ active: validRows > 0,
526
+ };
527
+ });
528
+ }
529
+
371
530
  function strongestSignal(state) {
372
531
  const mail = countStateRows(state, 'agent_mail.jsonl');
373
532
  const tasks = countStateRows(state, 'agent_tasks.jsonl');
374
533
  const scorecards = countStateRows(state, 'scorecards.jsonl');
375
534
  const episodes = countStateRows(state, ['episodes.jsonl', 'task_episodes.jsonl']);
376
- if (scorecards > 0 && episodes > 0) return `${scorecards} scorecard row(s) and ${episodes} episode row(s) are available for feedback-driven learning.`;
377
- if (scorecards > 0) return `${scorecards} scorecard row(s) are available for outcome scoring.`;
378
- if (episodes > 0) return `${episodes} episode row(s) are available; compile them into scorecards and next-action memory.`;
535
+ const activeLoops = (state.loopHealth || buildLoopHealth(state.stateFiles || []))
536
+ .filter(channel => channel.active);
537
+ const loopSuffix = activeLoops.length > 0
538
+ ? ` Loop health sees ${activeLoops.length} active channel(s): ${activeLoops.map(channel => channel.label).join(', ')}.`
539
+ : '';
540
+ if (scorecards > 0 && episodes > 0) return `${scorecards} scorecard row(s) and ${episodes} episode row(s) are available for feedback-driven learning.${loopSuffix}`;
541
+ if (scorecards > 0) return `${scorecards} scorecard row(s) are available for outcome scoring.${loopSuffix}`;
542
+ if (episodes > 0) return `${episodes} episode row(s) are available; compile them into scorecards and next-action memory.${loopSuffix}`;
543
+ if (activeLoops.length > 0) return `Loop health sees ${activeLoops.length} active channel(s): ${activeLoops.map(channel => channel.label).join(', ')}.`;
379
544
  if (mail > 0) return `${mail} agent-mail row(s) are available; compile them into decisions, follow-ups, and CRM memory.`;
380
545
  if (tasks > 0) return `${tasks} agent-task row(s) are available; use them to choose the next action.`;
381
546
  return 'Workspace has structure, but little scored state yet; first improvement is to create scorecards and episodes.';
@@ -440,10 +605,6 @@ function latestRecommendation(root) {
440
605
  return nextMove(collectState(root));
441
606
  }
442
607
 
443
- function loadBrainState(root) {
444
- return readJson(path.join(root, 'atris', 'brain', 'state.json')) || collectState(root);
445
- }
446
-
447
608
  function normalizeMemberSlug(memberSlug) {
448
609
  return String(memberSlug || '').toLowerCase().replace(/[^a-z0-9_-]/g, '');
449
610
  }
@@ -917,14 +1078,18 @@ function latestTaskEpisodes(taskEpisodes) {
917
1078
 
918
1079
  function recordTaskEpisodeScorecards(options) {
919
1080
  const root = options.root;
920
- const stateDir = path.join(root, '.atris', 'state');
921
- const taskEpisodesPath = path.join(stateDir, 'task_episodes.jsonl');
1081
+ const stateRoot = resolveStateRoot(root);
1082
+ const stateDir = path.join(stateRoot, '.atris', 'state');
1083
+ const stateDirs = resolveStateRoots(root).map(item => path.join(item, '.atris', 'state'));
922
1084
  const scorecardsPath = path.join(stateDir, 'scorecards.jsonl');
923
- const workspace = readJson(path.join(root, '.atris', 'business.json')) || {};
924
- const taskEpisodes = readJsonlRows(taskEpisodesPath)
1085
+ const workspace =
1086
+ readJson(path.join(stateRoot, '.atris', 'business.json')) ||
1087
+ readJson(path.join(root, '.atris', 'business.json')) ||
1088
+ {};
1089
+ const taskEpisodes = stateDirs.flatMap(item => readJsonlRows(path.join(item, 'task_episodes.jsonl')))
925
1090
  .filter(row => row && row.schema === 'atris.task_episode.v1');
926
1091
  const scoreableEpisodes = latestTaskEpisodes(taskEpisodes);
927
- const existing = readJsonlRows(scorecardsPath);
1092
+ const existing = stateDirs.flatMap(item => readJsonlRows(path.join(item, 'scorecards.jsonl')));
928
1093
  const seenEpisodeIds = new Set(existing
929
1094
  .map(row => row.source_episode_id)
930
1095
  .filter(Boolean));
@@ -940,6 +1105,7 @@ function recordTaskEpisodeScorecards(options) {
940
1105
  }
941
1106
 
942
1107
  return {
1108
+ stateRoot,
943
1109
  taskEpisodes: taskEpisodes.length,
944
1110
  written: written.length,
945
1111
  scorecards: written,
@@ -947,11 +1113,11 @@ function recordTaskEpisodeScorecards(options) {
947
1113
  }
948
1114
 
949
1115
  function verifyTaskEpisodeScorecards(root) {
950
- const stateDir = path.join(root, '.atris', 'state');
951
- const taskEpisodes = readJsonlRows(path.join(stateDir, 'task_episodes.jsonl'))
1116
+ const stateDirs = resolveStateRoots(root).map(item => path.join(item, '.atris', 'state'));
1117
+ const taskEpisodes = stateDirs.flatMap(item => readJsonlRows(path.join(item, 'task_episodes.jsonl')))
952
1118
  .filter(row => row && row.schema === 'atris.task_episode.v1');
953
1119
  const scoreableEpisodes = latestTaskEpisodes(taskEpisodes);
954
- const scorecards = readJsonlRows(path.join(stateDir, 'scorecards.jsonl'));
1120
+ const scorecards = stateDirs.flatMap(item => readJsonlRows(path.join(item, 'scorecards.jsonl')));
955
1121
  const scorecardEpisodeIds = new Set(scorecards
956
1122
  .map(row => row.source_episode_id)
957
1123
  .filter(Boolean));
@@ -997,20 +1163,39 @@ function renderBulletedLoadOrder(state) {
997
1163
  .join('\n');
998
1164
  }
999
1165
 
1166
+ function renderLoopHealthPanel(state) {
1167
+ const rows = (state.loopHealth || []).map(channel => {
1168
+ const status = channel.active ? 'active' : 'missing';
1169
+ return `| ${channel.label} | ${status} | ${channel.rows} | ${channel.validRows} | ${channel.latestTs || ''} | \`${channel.files.join('`, `')}\` |`;
1170
+ }).join('\n');
1171
+
1172
+ return `## Loop Health
1173
+
1174
+ | Channel | Status | Rows | Valid | Latest timestamp | Files |
1175
+ |---|---|---:|---:|---|---|
1176
+ ${rows}`;
1177
+ }
1178
+
1000
1179
  function renderStatus(state) {
1180
+ const stateRootLine = state.stateRoot && path.resolve(state.stateRoot) !== path.resolve(state.root)
1181
+ ? `- State root: ${state.stateRoot} (primary checkout)\n`
1182
+ : '';
1183
+
1001
1184
  return `# Atris Brain Status
1002
1185
 
1003
1186
  - Generated: ${state.generatedAt}
1004
1187
  - Workspace: ${state.name}
1005
1188
  - Slug: ${state.slug}
1006
1189
  - Root: ${state.root}
1007
- - Now loaded: ${state.hasNow ? `yes (${state.nowHeading || 'no heading'})` : 'no'}
1190
+ ${stateRootLine}- Now loaded: ${state.hasNow ? `yes (${state.nowHeading || 'no heading'})` : 'no'}
1008
1191
  - MAP loaded: ${state.hasMap ? `yes (${state.mapLineCount} lines)` : 'no'}
1009
1192
  - Wiki status loaded: ${state.hasWikiStatus ? 'yes' : 'no'}
1010
1193
  - TODO open estimate: ${state.todo.open}
1011
- - State rows: ${state.totalRows} raw / ${state.validRows} valid JSONL
1194
+ - State rows: ${state.totalRows} raw / ${state.validRows} valid state rows
1012
1195
  - Latest state timestamp: ${state.latestStateTs || 'none found'}
1013
1196
 
1197
+ ${renderLoopHealthPanel(state)}
1198
+
1014
1199
  ## What Improved
1015
1200
 
1016
1201
  This run compiled scattered workspace state into one loadable brain:
@@ -1043,7 +1228,7 @@ Definitions: operator = current person or agent; move = one concrete high-levera
1043
1228
 
1044
1229
  function renderLedger(state) {
1045
1230
  const rows = state.stateFiles.map(item => {
1046
- const rel = path.relative(state.root, item.path).replace(/\\/g, '/');
1231
+ const rel = path.relative(state.stateRoot || state.root, item.path).replace(/\\/g, '/');
1047
1232
  return `| \`${rel}\` | ${item.exists ? 'yes' : 'no'} | ${item.rows} | ${item.validRows} | ${item.latestTs || ''} |`;
1048
1233
  }).join('\n');
1049
1234
 
@@ -1059,7 +1244,7 @@ This is not model-weight improvement yet. It is workspace-policy and context imp
1059
1244
 
1060
1245
  ## Current State Inputs
1061
1246
 
1062
- | Source | Exists | Rows | Valid JSONL | Latest timestamp |
1247
+ | Source | Exists | Rows | Valid JSON/JSONL | Latest timestamp |
1063
1248
  |---|---:|---:|---:|---|
1064
1249
  ${rows}
1065
1250
 
@@ -1092,7 +1277,7 @@ function generatedBootBlock(state) {
1092
1277
  This workspace has a compiled agent brain.
1093
1278
 
1094
1279
  On session start, activate it first:
1095
- \`atris brain activate --root ${state.root} --verify\`
1280
+ \`atris brain activate --root . --verify\`
1096
1281
 
1097
1282
  Load these first:
1098
1283
  ${renderBulletedLoadOrder(state)}
@@ -1103,7 +1288,7 @@ Shape: \`<operator>, today is about <move>\` -> \`I picked this because <why now
1103
1288
  Definitions: operator = current person or agent; move = one concrete high-leverage workflow; why now = business reason; ready = prepared action or proof; paths = 2-4 optional deeper views.
1104
1289
 
1105
1290
  Re-run after meaningful work:
1106
- \`atris brain compile --root ${state.root}\`
1291
+ \`atris brain compile --root .\`
1107
1292
  ${GENERATED_END}
1108
1293
  `;
1109
1294
  }