atris 3.30.0 → 3.30.2

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 (69) hide show
  1. package/AGENTS.md +6 -0
  2. package/atris/AGENTS.md +11 -0
  3. package/atris/CLAUDE.md +5 -0
  4. package/atris/atris.md +19 -4
  5. package/atris/policies/atris-design.md +71 -0
  6. package/atris/policies/design-seed.md +187 -0
  7. package/atris/skills/atris/SKILL.md +26 -3
  8. package/atris/skills/design/SKILL.md +3 -2
  9. package/atris/skills/loop/SKILL.md +5 -3
  10. package/atris/team/_template/MEMBER.md +19 -0
  11. package/atris.md +63 -23
  12. package/ax +1434 -1698
  13. package/bin/atris.js +5 -1
  14. package/commands/agent-spawn.js +2 -2
  15. package/commands/brain.js +92 -7
  16. package/commands/brainstorm.js +62 -22
  17. package/commands/business-sync.js +92 -8
  18. package/commands/business.js +13 -7
  19. package/commands/chat-scan.js +102 -0
  20. package/commands/computer.js +758 -15
  21. package/commands/deck.js +505 -105
  22. package/commands/init.js +6 -1
  23. package/commands/launchpad.js +638 -0
  24. package/commands/log-sync.js +44 -13
  25. package/commands/loop-front.js +290 -0
  26. package/commands/member.js +124 -3
  27. package/commands/mission.js +653 -30
  28. package/commands/pull.js +37 -28
  29. package/commands/pulse.js +11 -8
  30. package/commands/run.js +79 -39
  31. package/commands/sync.js +36 -17
  32. package/commands/task.js +1072 -89
  33. package/commands/workflow.js +6 -6
  34. package/commands/xp.js +13 -1
  35. package/commands/youtube.js +221 -7
  36. package/decks/README.md +89 -0
  37. package/decks/archetype-catalog.json +180 -0
  38. package/decks/atris-antislop-pitch.json +48 -0
  39. package/decks/atris-archetypes-v2.json +83 -0
  40. package/decks/atris-one-loop-pitch.json +80 -0
  41. package/decks/atris-seed-pitch-v3.json +118 -0
  42. package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
  43. package/decks/atris-seed-pitch-v5.json +109 -0
  44. package/decks/atris-seed-pitch-v6.json +137 -0
  45. package/decks/atris-seed-pitch-v7.json +133 -0
  46. package/decks/atris-single-shot-proof.json +74 -0
  47. package/decks/mark-pincus-narrative.json +102 -0
  48. package/decks/mark-pincus-sourcery.json +94 -0
  49. package/decks/yash-applied-compute-detailed.json +150 -0
  50. package/decks/yash-applied-compute-generalist.json +82 -0
  51. package/decks/yash-applied-compute-narrative.json +54 -0
  52. package/lib/ax-prefs.js +5 -12
  53. package/lib/chat-log-scan.js +377 -0
  54. package/lib/context-gatherer.js +35 -1
  55. package/lib/deck-compose.js +145 -0
  56. package/lib/deck-history.js +64 -0
  57. package/lib/deck-layout.js +169 -0
  58. package/lib/deck-review.js +431 -0
  59. package/lib/deck-schema.js +154 -0
  60. package/lib/file-ops.js +3 -3
  61. package/lib/functional-owner.js +189 -0
  62. package/lib/journal.js +7 -73
  63. package/lib/slides-deck.js +512 -58
  64. package/lib/task-db.js +128 -11
  65. package/package.json +2 -1
  66. package/templates/business-starter/team/START_HERE.md +12 -8
  67. package/utils/auth.js +4 -0
  68. package/utils/config.js +4 -0
  69. package/atris/atrisDev.md +0 -717
package/bin/atris.js CHANGED
@@ -369,6 +369,7 @@ function showHelp() {
369
369
  console.log(' activate - Load Atris context');
370
370
  console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
371
371
  console.log(' ctop - Show a process-first live agent CPU/memory view');
372
+ console.log(' launchpad - Show the next action from local brain, task, mission, and proof state');
372
373
  console.log(' status - See local work and completions (`atris status <business>` for remote)');
373
374
  console.log(' recap - What your AI team did, in plain English (--share for paste-ready)');
374
375
  console.log(' xp - Show Career XP and contribution graph');
@@ -820,7 +821,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
820
821
  }
821
822
 
822
823
  // Check if this is a known command or natural language input
823
- const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
824
+ const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
824
825
  'activate', '_activate', 'agent', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
825
826
  'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
826
827
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap', 'signup', 'clarity', 'moves',
@@ -1702,6 +1703,9 @@ if (command === 'init') {
1702
1703
  console.error(`\u2717 Run failed: ${error.message || error}`);
1703
1704
  process.exit(1);
1704
1705
  });
1706
+ } else if (command === 'launchpad') {
1707
+ const code = require('../commands/launchpad').launchpadCommand(process.argv.slice(3));
1708
+ process.exit(code);
1705
1709
  } else if (command === 'autopilot') {
1706
1710
  const args = process.argv.slice(3);
1707
1711
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
@@ -350,9 +350,9 @@ function writeDogfoodReceipt(root, receipt) {
350
350
  }
351
351
 
352
352
  function showDogfoodHelp(output = console.log) {
353
- output('Usage: atris agent dogfood [devin|droid|--engine all] [--model glm-5.2] [--live] [--json]');
353
+ output('Internal usage: atris agent dogfood [devin|droid|--engine all] [--model glm-5.2] [--live] [--json]');
354
354
  output('');
355
- output('Cheaply verifies external coding CLIs without a model call by default.');
355
+ output('Internal support smoke for external coding CLIs; hidden from public agent help.');
356
356
  output('Add --live to run one exact sentinel prompt through Devin/Droid.');
357
357
  }
358
358
 
package/commands/brain.js CHANGED
@@ -13,6 +13,9 @@ const GENERATED_LOAD_ORDER_FILES = [
13
13
  'atris/brain/STATUS.md',
14
14
  'atris/brain/self_improvement_ledger.md',
15
15
  ];
16
+ const OPTIONAL_STATE_LOAD_ORDER_FILES = [
17
+ '.atris/state/chat_scan.latest.json',
18
+ ];
16
19
  const OPTIONAL_LOAD_ORDER_FILES = [
17
20
  'atris/wiki/concepts/agent-activation-contract.md',
18
21
  'atris/skills/atris/SKILL.md',
@@ -1128,10 +1131,68 @@ function taskEpisodeScorecard(root, episode, workspace, ts = new Date().toISOStr
1128
1131
  lesson: episode.lesson || '',
1129
1132
  proof: episode.proof || '',
1130
1133
  next_task_suggestion: episode.next_task_suggestion || null,
1134
+ review_landing: episode.review_landing || null,
1135
+ landing_quality: episode.landing_quality || null,
1136
+ human_feedback: episode.human_feedback || null,
1137
+ approval_status: episode.human_feedback && episode.human_feedback.approval_status || episode.rl && episode.rl.approval_status || null,
1138
+ rl_label: episode.rl && episode.rl.label || null,
1131
1139
  episode_created_at: episode.created_at || episode.ts || null,
1132
1140
  };
1133
1141
  }
1134
1142
 
1143
+ function landingQualityFromReviewLanding(landing) {
1144
+ const normalized = {
1145
+ happened: landing && landing.happened || null,
1146
+ checked: landing && landing.checked || null,
1147
+ tested: landing && landing.tested || null,
1148
+ decision: landing && landing.decision || null,
1149
+ };
1150
+ const present = Object.entries(normalized)
1151
+ .filter(([, value]) => Boolean(value))
1152
+ .map(([key]) => key);
1153
+ const missing = Object.keys(normalized).filter(key => !normalized[key]);
1154
+ return {
1155
+ present,
1156
+ missing,
1157
+ completeness: present.length / Object.keys(normalized).length,
1158
+ has_decision: Boolean(normalized.decision),
1159
+ };
1160
+ }
1161
+
1162
+ function taskProjectionByEpisodeId(root) {
1163
+ const tasks = readTaskProjectionTasks(root) || [];
1164
+ const byId = new Map();
1165
+ for (const task of tasks) {
1166
+ for (const key of [task.id, task.display_id, task.legacy_ref, task.ref].filter(Boolean)) {
1167
+ byId.set(String(key), task);
1168
+ }
1169
+ }
1170
+ return byId;
1171
+ }
1172
+
1173
+ function enrichTaskEpisodeFromProjection(episode, taskById) {
1174
+ if (episode.review_landing) return episode;
1175
+ const task = taskById.get(String(episode.task_id || ''));
1176
+ const landing = task?.review?.landing || null;
1177
+ if (!landing) return episode;
1178
+ const approvalStatus = task?.review?.approval_status || episode.human_feedback?.approval_status || episode.rl?.approval_status || null;
1179
+ return {
1180
+ ...episode,
1181
+ review_landing: landing,
1182
+ landing_quality: episode.landing_quality || landingQualityFromReviewLanding(landing),
1183
+ human_feedback: episode.human_feedback || {
1184
+ approval_status: approvalStatus,
1185
+ human_revision_count: Number(task?.review?.human_revision_count || 0),
1186
+ human_revision_note: task?.review?.human_revision_note || null,
1187
+ },
1188
+ rl: episode.rl ? {
1189
+ ...episode.rl,
1190
+ landing_completeness: episode.rl.landing_completeness ?? landingQualityFromReviewLanding(landing).completeness,
1191
+ approval_status: episode.rl.approval_status || approvalStatus,
1192
+ } : episode.rl,
1193
+ };
1194
+ }
1195
+
1135
1196
  function latestTaskEpisodes(taskEpisodes) {
1136
1197
  const byTask = new Map();
1137
1198
  for (const episode of taskEpisodes) {
@@ -1154,19 +1215,31 @@ function recordTaskEpisodeScorecards(options) {
1154
1215
  {};
1155
1216
  const taskEpisodes = stateDirs.flatMap(item => readJsonlRows(path.join(item, 'task_episodes.jsonl')))
1156
1217
  .filter(row => row && row.schema === 'atris.task_episode.v1');
1157
- const scoreableEpisodes = latestTaskEpisodes(taskEpisodes);
1218
+ const taskById = taskProjectionByEpisodeId(stateRoot);
1219
+ if (stateRoot !== root) {
1220
+ for (const [key, task] of taskProjectionByEpisodeId(root)) taskById.set(key, task);
1221
+ }
1222
+ const scoreableEpisodes = latestTaskEpisodes(taskEpisodes)
1223
+ .map(episode => enrichTaskEpisodeFromProjection(episode, taskById));
1158
1224
  const existing = stateDirs.flatMap(item => readJsonlRows(path.join(item, 'scorecards.jsonl')));
1159
- const seenEpisodeIds = new Set(existing
1160
- .map(row => row.source_episode_id)
1161
- .filter(Boolean));
1225
+ const existingByEpisodeId = new Map();
1226
+ for (const row of existing) {
1227
+ if (!row.source_episode_id) continue;
1228
+ const rows = existingByEpisodeId.get(row.source_episode_id) || [];
1229
+ rows.push(row);
1230
+ existingByEpisodeId.set(row.source_episode_id, rows);
1231
+ }
1162
1232
 
1163
1233
  const written = [];
1164
1234
  for (const episode of scoreableEpisodes) {
1165
1235
  const episodeId = episode.episode_id;
1166
- if (seenEpisodeIds.has(episodeId)) continue;
1167
1236
  const scorecard = taskEpisodeScorecard(root, episode, workspace);
1237
+ const existingRows = existingByEpisodeId.get(episodeId) || [];
1238
+ const existingHasLanding = existingRows.some(row => row.review_landing);
1239
+ if (existingRows.length && (!scorecard.review_landing || existingHasLanding)) continue;
1168
1240
  appendJsonl(scorecardsPath, scorecard);
1169
- seenEpisodeIds.add(episodeId);
1241
+ existingRows.push(scorecard);
1242
+ existingByEpisodeId.set(episodeId, existingRows);
1170
1243
  written.push(scorecard);
1171
1244
  }
1172
1245
 
@@ -1212,9 +1285,21 @@ function verifyApproval(root, approvalId) {
1212
1285
 
1213
1286
  function brainLoadOrderFiles(state) {
1214
1287
  const root = state.root;
1288
+ const lookupRoots = [root, ...(state.stateRoots || []), state.stateRoot]
1289
+ .filter(Boolean)
1290
+ .map(item => path.resolve(item));
1291
+ const seen = new Set();
1292
+ const uniqueRoots = lookupRoots.filter(item => {
1293
+ if (seen.has(item)) return false;
1294
+ seen.add(item);
1295
+ return true;
1296
+ });
1297
+ const loadOrderFileExists = rel => uniqueRoots.some(base => fs.existsSync(path.join(base, rel)));
1298
+ const existingState = OPTIONAL_STATE_LOAD_ORDER_FILES
1299
+ .filter(loadOrderFileExists);
1215
1300
  const existing = OPTIONAL_LOAD_ORDER_FILES
1216
1301
  .filter(rel => fs.existsSync(path.join(root, rel)));
1217
- return [...GENERATED_LOAD_ORDER_FILES, ...existing];
1302
+ return [...GENERATED_LOAD_ORDER_FILES, ...existingState, ...existing];
1218
1303
  }
1219
1304
 
1220
1305
  function renderNumberedLoadOrder(state) {
@@ -321,29 +321,43 @@ async function brainstormAtris() {
321
321
  const nextSteps = nextStepsRaw
322
322
  ? nextStepsRaw.split(';').map((item) => item.trim()).filter(Boolean)
323
323
  : [];
324
- recordBrainstormSession(
325
- logFile,
326
- sourceLabel,
327
- topicSummary,
328
- userStory,
329
- [],
330
- [],
331
- constraints,
332
- '',
333
- feelingsVibe || '',
334
- nextSteps,
335
- sessionSummary
336
- );
337
- if (selectedInboxItem) {
338
- const archive = await askYesNo('Archive this Inbox idea now? (y/n): ');
339
- if (archive) {
340
- let latestContent = fs.readFileSync(logFile, 'utf8');
341
- latestContent = removeInboxItemFromContent(latestContent, selectedInboxItem.id);
342
- fs.writeFileSync(logFile, latestContent);
343
- console.log(`✓ Archived I${selectedInboxItem.id} from Inbox.`);
324
+ try {
325
+ recordBrainstormSession(
326
+ logFile,
327
+ sourceLabel,
328
+ topicSummary,
329
+ userStory,
330
+ [],
331
+ [],
332
+ constraints,
333
+ '',
334
+ feelingsVibe || '',
335
+ nextSteps,
336
+ sessionSummary
337
+ );
338
+ if (selectedInboxItem) {
339
+ const archive = await askYesNo('Archive this Inbox idea now? (y/n): ');
340
+ if (archive) {
341
+ try {
342
+ let latestContent = fs.readFileSync(logFile, 'utf8');
343
+ latestContent = removeInboxItemFromContent(latestContent, selectedInboxItem.id);
344
+ if (typeof latestContent !== 'string') {
345
+ throw new Error('Archive operation produced invalid journal content.');
346
+ }
347
+ writeJournalFile(logFile, latestContent);
348
+ console.log(`✓ Archived I${selectedInboxItem.id} from Inbox.`);
349
+ } catch (error) {
350
+ console.log(`Could not archive I${selectedInboxItem.id}: ${error.message}`);
351
+ }
352
+ }
353
+ }
354
+ console.log('✓ Brainstorm session logged.');
355
+ } catch (error) {
356
+ if (error && error.__brainstormAbort) {
357
+ throw error;
344
358
  }
359
+ console.log(`Could not log brainstorm session: ${error.message}`);
345
360
  }
346
- console.log('✓ Brainstorm session logged.');
347
361
  } else {
348
362
  console.log('Skipped journaling. Prompt is ready for your agent.');
349
363
  }
@@ -365,6 +379,32 @@ function removeInboxItemFromContent(content, id) {
365
379
  return replaceInboxSection(content, items);
366
380
  }
367
381
 
382
+ function writeJournalFile(logFile, content) {
383
+ if (typeof logFile !== 'string' || logFile.trim() === '') {
384
+ throw new Error('Journal log file path is required.');
385
+ }
386
+ if (typeof content !== 'string') {
387
+ throw new Error('Journal content must be a string.');
388
+ }
389
+ const logDir = path.dirname(logFile);
390
+ const tempFile = path.join(logDir, `.${path.basename(logFile)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
391
+ let tempFileWritten = false;
392
+ try {
393
+ fs.mkdirSync(logDir, { recursive: true });
394
+ fs.writeFileSync(tempFile, content, { encoding: 'utf8', flag: 'wx' });
395
+ tempFileWritten = true;
396
+ fs.renameSync(tempFile, logFile);
397
+ } catch (error) {
398
+ if (tempFileWritten || !error || error.code !== 'EEXIST') {
399
+ try {
400
+ fs.unlinkSync(tempFile);
401
+ } catch {}
402
+ }
403
+ const message = error && error.message ? error.message : String(error);
404
+ throw new Error(`Could not write journal file: ${message}`);
405
+ }
406
+ }
407
+
368
408
  function insertIntoNotesSection(content, block) {
369
409
  const regex = /(## Notes\n)([\s\S]*?)(\n---|\n##|$)/;
370
410
  const match = content.match(regex);
@@ -437,7 +477,7 @@ function recordBrainstormSession(
437
477
 
438
478
  const block = lines.join('\n');
439
479
  content = insertIntoNotesSection(content, block);
440
- fs.writeFileSync(logFile, content);
480
+ writeJournalFile(logFile, content);
441
481
  }
442
482
 
443
483
 
@@ -1,7 +1,8 @@
1
1
  const { spawn } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const { loadManifest } = require('../lib/manifest');
4
+ const { loadManifest, saveManifest } = require('../lib/manifest');
5
+ const { removeBaseContents, writeBaseContents } = require('../lib/company-brain-sync');
5
6
 
6
7
  const WATCH_IGNORED_DIRS = new Set([
7
8
  '.git', '.atris', '.claude', '.cursor', '.next', '.cache',
@@ -29,6 +30,7 @@ function parseBusinessSyncArgs(args = []) {
29
30
  const resolveIdx = positional.indexOf('resolve');
30
31
  const resolveFlag = parseFlagValue(args, '--resolve', null);
31
32
  const resolve = resolveFlag || (resolveIdx !== -1 ? positional[resolveIdx + 1] : null);
33
+ const resolvePath = parseFlagValue(args, '--path', parseFlagValue(args, '--file', null));
32
34
  const commandWords = new Set(['status', 'doctor', 'review', 'resolve', 'local', 'cloud', 'both', 'merge']);
33
35
  const slug = positional.find((arg) => !commandWords.has(arg)) || null;
34
36
  const dryRun = args.includes('--dry-run');
@@ -39,7 +41,7 @@ function parseBusinessSyncArgs(args = []) {
39
41
  const debounceSec = Number.parseInt(parseFlagValue(args, '--debounce', '5'), 10);
40
42
  const help = args.includes('--help') || args.includes('-h') || positional[0] === 'help';
41
43
 
42
- return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, help };
44
+ return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, resolvePath, help };
43
45
  }
44
46
 
45
47
  function readBusinessSlug(cwd = process.cwd()) {
@@ -332,7 +334,7 @@ function renderLocalSyncStatus(status) {
332
334
 
333
335
  function renderBusinessSyncHelp() {
334
336
  return [
335
- 'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--review] [--resolve local|cloud|both|merge] [--timeout 120]',
337
+ 'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--review] [--resolve local|cloud|both|merge] [--path <file>] [--timeout 120]',
336
338
  '',
337
339
  'Safe loop:',
338
340
  ' Pull -> Review -> Publish',
@@ -341,7 +343,7 @@ function renderBusinessSyncHelp() {
341
343
  ' atris sync --status Show local sync health',
342
344
  ' atris sync --dry-run Preview pull and publish plans without writing cloud',
343
345
  ' atris sync --review Read the latest conflict packet',
344
- ' atris sync --resolve cloud|local|merge',
346
+ ' atris sync --resolve cloud|local|merge [--path <file>]',
345
347
  ' atris sync --watch Keep the workspace live with the same safety gates',
346
348
  '',
347
349
  'Publish safety:',
@@ -398,6 +400,7 @@ function collectConflictResolutionEntries(cwd = process.cwd()) {
398
400
  const targetRel = path.relative(dir, full).replace(/\\/g, '/').replace(/\.local$/, '');
399
401
  entries.push({
400
402
  targetRel,
403
+ packetDir: dir,
401
404
  basePath: full.replace(/\.local$/, '.base'),
402
405
  localPath,
403
406
  remotePath,
@@ -426,6 +429,73 @@ function cleanupResolvedConflictSidecars(cwd, targetRel, { keepCloud = false } =
426
429
  }
427
430
  }
428
431
 
432
+ function cleanupConflictPacketArtifacts(entry) {
433
+ for (const artifactPath of [entry.basePath, entry.localPath, entry.remotePath]) {
434
+ fs.rmSync(artifactPath, { force: true });
435
+ }
436
+ }
437
+
438
+ function conflictPacketHasEntries(packetDir) {
439
+ let hasEntries = false;
440
+
441
+ function walk(current) {
442
+ if (hasEntries) return;
443
+ let items;
444
+ try {
445
+ items = fs.readdirSync(current, { withFileTypes: true });
446
+ } catch {
447
+ return;
448
+ }
449
+ for (const item of items) {
450
+ const full = path.join(current, item.name);
451
+ if (item.isDirectory()) walk(full);
452
+ else if (item.isFile() && item.name.endsWith('.local')) {
453
+ hasEntries = true;
454
+ return;
455
+ }
456
+ if (hasEntries) return;
457
+ }
458
+ }
459
+
460
+ walk(packetDir);
461
+ return hasEntries;
462
+ }
463
+
464
+ function finalizeResolvedConflictPacket(cwd, packetDir) {
465
+ const pendingPath = path.join(packetDir, 'manifest.json');
466
+ const pending = readJsonFile(pendingPath);
467
+ if (!pending || !pending.manifest || !pending.manifest.files) return null;
468
+
469
+ const slug = pending.slug || readBusinessSlug(cwd);
470
+ if (!slug) return null;
471
+
472
+ const manifest = {
473
+ ...pending.manifest,
474
+ workspace_root: pending.manifest.workspace_root || cwd,
475
+ };
476
+ saveManifest(slug, manifest);
477
+ if (pending.baseContents && typeof pending.baseContents === 'object') {
478
+ writeBaseContents(cwd, pending.baseContents);
479
+ }
480
+ if (Array.isArray(pending.deletedRemote)) {
481
+ removeBaseContents(cwd, pending.deletedRemote);
482
+ }
483
+ return slug;
484
+ }
485
+
486
+ function cleanupEmptyConflictPackets(cwd, entries) {
487
+ const finalized = [];
488
+ const packetDirs = Array.from(new Set(entries.map((entry) => entry.packetDir).filter(Boolean)));
489
+ for (const packetDir of packetDirs) {
490
+ if (!conflictPacketHasEntries(packetDir)) {
491
+ const slug = finalizeResolvedConflictPacket(cwd, packetDir);
492
+ if (slug) finalized.push(slug);
493
+ fs.rmSync(packetDir, { recursive: true, force: true });
494
+ }
495
+ }
496
+ return finalized;
497
+ }
498
+
429
499
  function changedRange(baseLines, changedLines) {
430
500
  let start = 0;
431
501
  while (start < baseLines.length && start < changedLines.length && baseLines[start] === changedLines[start]) {
@@ -562,16 +632,24 @@ function safeMarkdownMerge(baseContent, localContent, remoteContent) {
562
632
  return { ok: true, content: merged, mode: 'markdown' };
563
633
  }
564
634
 
565
- function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
635
+ function normalizeResolvePath(value) {
636
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
637
+ }
638
+
639
+ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local', target = null) {
566
640
  if (!['local', 'cloud', 'both', 'merge'].includes(strategy)) {
567
641
  throw new Error('Use `atris sync --resolve local`, `atris sync --resolve cloud`, `atris sync --resolve both`, or `atris sync --resolve merge`.');
568
642
  }
569
643
 
570
- const entries = collectConflictResolutionEntries(cwd);
644
+ const wantedTarget = normalizeResolvePath(target);
645
+ const entries = collectConflictResolutionEntries(cwd)
646
+ .filter((entry) => !wantedTarget || normalizeResolvePath(entry.targetRel) === wantedTarget);
571
647
  if (entries.length === 0) {
572
648
  return {
573
649
  resolved: [],
574
- message: 'No sync conflicts need resolution.\n',
650
+ message: wantedTarget
651
+ ? `No sync conflicts need resolution for ${wantedTarget}.\n`
652
+ : 'No sync conflicts need resolution.\n',
575
653
  };
576
654
  }
577
655
 
@@ -591,6 +669,7 @@ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
591
669
  fs.copyFileSync(entry.remotePath, remoteCopyPath);
592
670
  }
593
671
  cleanupResolvedConflictSidecars(cwd, entry.targetRel, { keepCloud: true });
672
+ cleanupConflictPacketArtifacts(entry);
594
673
  resolved.push(entry.targetRel);
595
674
  continue;
596
675
  }
@@ -612,6 +691,7 @@ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
612
691
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
613
692
  fs.writeFileSync(targetPath, merged.content, 'utf8');
614
693
  cleanupResolvedConflictSidecars(cwd, entry.targetRel);
694
+ cleanupConflictPacketArtifacts(entry);
615
695
  resolved.push(entry.targetRel);
616
696
  continue;
617
697
  }
@@ -621,15 +701,19 @@ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
621
701
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
622
702
  fs.copyFileSync(sourcePath, targetPath);
623
703
  cleanupResolvedConflictSidecars(cwd, entry.targetRel);
704
+ cleanupConflictPacketArtifacts(entry);
624
705
  resolved.push(entry.targetRel);
625
706
  }
626
707
 
708
+ const finalizedManifests = cleanupEmptyConflictPackets(cwd, entries);
709
+
627
710
  return {
628
711
  resolved,
629
712
  unresolved,
630
713
  message: [
631
714
  `Resolved ${resolved.length} conflict${resolved.length === 1 ? '' : 's'} using ${strategy === 'both' ? 'both versions' : `${strategy === 'merge' ? 'safe merge' : `${strategy === 'local' ? 'local' : 'cloud'} version`}`}.`,
632
715
  ...resolved.map((rel) => ` - ${rel}`),
716
+ ...(finalizedManifests.length ? ['', `Updated sync manifest for ${finalizedManifests.join(', ')}.`] : []),
633
717
  ...(unresolved.length ? ['', 'Still needs review:', ...unresolved.map((rel) => ` - ${rel}`)] : []),
634
718
  '',
635
719
  'Next: run `atris sync --dry-run` before publishing.',
@@ -717,7 +801,7 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
717
801
  }
718
802
 
719
803
  if (options.resolve) {
720
- process.stdout.write(resolveLatestConflict(cwd, options.resolve).message);
804
+ process.stdout.write(resolveLatestConflict(cwd, options.resolve, options.resolvePath).message);
721
805
  return;
722
806
  }
723
807
 
@@ -867,7 +867,6 @@ function renderBusinessMissingAction(missing) {
867
867
  }
868
868
 
869
869
  function renderBusinessCreatedNextSteps(bizMeta = {}, workspaceRoot = '.') {
870
- const slug = bizMeta.slug || slugifyName(bizMeta.name) || 'business';
871
870
  const lines = [
872
871
  ' Atris: seeded local computer + operator + validator',
873
872
  '',
@@ -887,7 +886,8 @@ function renderBusinessCreatedNextSteps(bizMeta = {}, workspaceRoot = '.') {
887
886
  ' atris business share --write',
888
887
  '',
889
888
  ' Sync when ready:',
890
- ` atris align ${slug} --fix`,
889
+ ' atris sync --dry-run',
890
+ ' atris sync',
891
891
  ];
892
892
  return lines.join('\n');
893
893
  }
@@ -926,6 +926,7 @@ function renderBusinessShareHandoff(state, options = {}) {
926
926
  '```bash',
927
927
  `atris pull ${bizMeta.slug}`,
928
928
  `cd ${bizMeta.slug}`,
929
+ 'atris sync --dry-run',
929
930
  'atris business start',
930
931
  '```',
931
932
  ] : [
@@ -959,13 +960,13 @@ function renderBusinessShareHandoff(state, options = {}) {
959
960
  '',
960
961
  '```bash',
961
962
  'atris business onboard --website <url> --contact "Name" --note "what changed"',
962
- 'atris pull --dry-run',
963
+ state.remoteReady ? 'atris sync --dry-run' : '# local-only: no cloud sync is available yet',
963
964
  'atris task next',
964
965
  ...renderBusinessMissionBootstrapLines(bizMeta),
965
966
  'atris do',
966
967
  'atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
967
968
  'atris business share --write',
968
- 'atris align --fix',
969
+ state.remoteReady ? 'atris sync' : '# local-only: share the folder directly',
969
970
  '```',
970
971
  '',
971
972
  '## Proof State',
@@ -984,6 +985,8 @@ function renderBusinessShareHandoff(state, options = {}) {
984
985
  'Useful commands:',
985
986
  '',
986
987
  '```bash',
988
+ state.remoteReady ? 'atris sync --status' : '# local-only: no cloud sync status yet',
989
+ state.remoteReady ? 'atris sync --watch' : '# local-only: no cloud watcher yet',
987
990
  'atris radar',
988
991
  'atris task next',
989
992
  ...renderBusinessMissionBootstrapLines(bizMeta),
@@ -1027,7 +1030,7 @@ function renderBusinessStartCard(state, options = {}) {
1027
1030
  `- ${state.onePager || 'missing operator one-pager'}`,
1028
1031
  '',
1029
1032
  'Run:',
1030
- state.remoteReady ? ' atris pull --dry-run' : ' # local-only: no cloud pull is available yet',
1033
+ state.remoteReady ? ' atris sync --dry-run' : ' # local-only: no cloud sync is available yet',
1031
1034
  ' atris',
1032
1035
  ' atris business start',
1033
1036
  ' atris radar',
@@ -1037,7 +1040,8 @@ function renderBusinessStartCard(state, options = {}) {
1037
1040
  ' atris do',
1038
1041
  ' atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
1039
1042
  ' atris business share --write',
1040
- ' atris align --fix',
1043
+ state.remoteReady ? ' atris sync' : ' # local-only: share the folder directly',
1044
+ state.remoteReady ? ' atris sync --watch' : ' # local-only: no cloud watcher yet',
1041
1045
  '',
1042
1046
  'Proof:',
1043
1047
  `- team lanes: ${state.teamMembers}`,
@@ -2833,12 +2837,14 @@ async function quickstart() {
2833
2837
  atris business share --write
2834
2838
 
2835
2839
  9. Push local state to cloud:
2836
- atris align --fix
2840
+ atris sync --dry-run
2841
+ atris sync
2837
2842
 
2838
2843
  Repeat:
2839
2844
  atris radar -> atris task next -> atris do -> record -> share
2840
2845
 
2841
2846
  Optional:
2847
+ atris sync --watch
2842
2848
  atris business connect slack --business my-company
2843
2849
  atris business connect github --business my-company
2844
2850
 
@@ -0,0 +1,102 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { scanChatLogs, writeLatestScan } = require('../lib/chat-log-scan');
4
+
5
+ function hasFlag(args, name) {
6
+ return args.includes(name);
7
+ }
8
+
9
+ function readFlag(args, name, fallback) {
10
+ const i = args.indexOf(name);
11
+ if (i === -1 || i === args.length - 1) return fallback;
12
+ return args[i + 1];
13
+ }
14
+
15
+ function printReport(report) {
16
+ console.log('');
17
+ console.log('┌ Chat scan ─────────────────────────────────────────────────┐');
18
+ console.log(`│ sessions ${String(report.summary.sessions).padEnd(3)} │ ax ${String(report.summary.ax_sessions).padEnd(3)} │ cursor ${String(report.summary.cursor_sessions).padEnd(3)} │ findings ${String(report.summary.findings).padEnd(3)} │`);
19
+ console.log('└────────────────────────────────────────────────────────────┘');
20
+ console.log('');
21
+ console.log(`Window: last ${report.window_hours}h`);
22
+ console.log(`Ax logs: ${report.sources.ax_dirs.join(', ') || '(none)'}`);
23
+ console.log(`Cursor: ${report.sources.cursor_roots.join(', ') || '(none)'}`);
24
+ console.log('');
25
+
26
+ if (!report.sessions.length) {
27
+ console.log('No recent chat logs found.');
28
+ console.log(' ax --pro --chat writes atris/runs/ax-play-*.log');
29
+ console.log(' Cursor writes ~/.cursor/projects/<slug>/agent-transcripts/*.jsonl');
30
+ return;
31
+ }
32
+
33
+ console.log('Recent sessions:');
34
+ for (const session of report.sessions.slice(0, 8)) {
35
+ const label = session.source === 'ax' ? `ax/${session.mode || '?'}` : 'cursor';
36
+ const when = session.started_at || path.basename(session.path);
37
+ const last = session.last_user || session.last_assistant || '(no text captured)';
38
+ console.log(`- [${label}] ${when}`);
39
+ console.log(` last: ${last}`);
40
+ if (session.interrupted) console.log(' signal: interrupted');
41
+ if (session.log_findings?.length) console.log(` errors: ${session.log_findings[0]}`);
42
+ }
43
+
44
+ if (report.findings.length) {
45
+ console.log('');
46
+ console.log('Problems detected:');
47
+ for (const finding of report.findings.slice(0, 8)) {
48
+ console.log(`- [${finding.severity}] ${finding.title}`);
49
+ if (finding.evidence) console.log(` ${finding.evidence}`);
50
+ }
51
+ console.log('');
52
+ console.log(`Next: ${report.next_command}`);
53
+ } else {
54
+ console.log('');
55
+ console.log('No problem patterns detected in recent chat logs.');
56
+ }
57
+ }
58
+
59
+ function chatScanCommand(argv = []) {
60
+ const args = argv.filter((a) => a !== '--');
61
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
62
+ console.log(`Usage: atris chat scan [--json] [--hours <n>] [--limit <n>] [--no-write]
63
+
64
+ Read recent ax chat logs and Cursor agent transcripts; surface friction/errors.
65
+
66
+ Options:
67
+ --hours <n> Lookback window (default 720 = 30 days)
68
+ --limit <n> Max sessions per source (default 12)
69
+ --no-write Skip writing .atris/state/chat_scan.latest.json
70
+ --json Machine-readable output`);
71
+ return { ok: true };
72
+ }
73
+
74
+ const root = process.cwd();
75
+ if (!fs.existsSync(path.join(root, 'atris'))) {
76
+ console.error('✗ atris/ folder not found. Run from an Atris workspace.');
77
+ process.exit(1);
78
+ }
79
+
80
+ const report = scanChatLogs({
81
+ cwd: root,
82
+ hours: readFlag(args, '--hours', 720),
83
+ limit: readFlag(args, '--limit', 12),
84
+ });
85
+
86
+ let latestPath = null;
87
+ if (!hasFlag(args, '--no-write')) {
88
+ latestPath = writeLatestScan(root, report);
89
+ report.latest_path = latestPath;
90
+ }
91
+
92
+ if (hasFlag(args, '--json')) {
93
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
94
+ return report;
95
+ }
96
+
97
+ printReport(report);
98
+ if (latestPath) console.log(`\nWrote ${path.relative(root, latestPath)}`);
99
+ return report;
100
+ }
101
+
102
+ module.exports = { chatScanCommand };