atris 3.37.0 → 3.38.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 (139) hide show
  1. package/README.md +2 -0
  2. package/atris/PERSONA.md +3 -7
  3. package/atris.md +10 -2
  4. package/ax +158 -33
  5. package/bin/atris.js +49 -2
  6. package/commands/activate.js +16 -3
  7. package/commands/agent-spawn.js +0 -7
  8. package/commands/autoland.js +92 -14
  9. package/commands/autopilot-front.js +0 -4
  10. package/commands/autopilot.js +0 -12
  11. package/commands/brain.js +0 -7
  12. package/commands/brief.js +0 -2
  13. package/commands/business-sync.js +0 -1
  14. package/commands/business.js +107 -12
  15. package/commands/clean.js +31 -2
  16. package/commands/close.js +46 -11
  17. package/commands/cloud.js +0 -1
  18. package/commands/codex-goal.js +0 -2
  19. package/commands/computer.js +6 -5
  20. package/commands/console.js +46 -9
  21. package/commands/deck.js +29 -8
  22. package/commands/dream.js +0 -7
  23. package/commands/drill.js +0 -1
  24. package/commands/engine.js +10 -18
  25. package/commands/experiments.js +0 -4
  26. package/commands/fleet-report.js +12 -4
  27. package/commands/game.js +261 -0
  28. package/commands/gm.js +10 -2
  29. package/commands/goal.js +0 -6
  30. package/commands/harvest.js +0 -1
  31. package/commands/improve.js +29 -8
  32. package/commands/init.js +18 -34
  33. package/commands/integrations.js +0 -7
  34. package/commands/land.js +71 -8
  35. package/commands/launchpad.js +0 -2
  36. package/commands/lesson.js +17 -7
  37. package/commands/loop-front.js +0 -2
  38. package/commands/loops.js +6 -5
  39. package/commands/meet.js +0 -2
  40. package/commands/member.js +68 -3
  41. package/commands/mission.js +359 -102
  42. package/commands/next.js +0 -1
  43. package/commands/now.js +0 -12
  44. package/commands/one-lap.js +8 -8
  45. package/commands/orb.js +0 -4
  46. package/commands/pack-craft.js +0 -1
  47. package/commands/pack.js +670 -77
  48. package/commands/play.js +0 -2
  49. package/commands/probe.js +10 -4
  50. package/commands/pulse.js +73 -48
  51. package/commands/push.js +0 -4
  52. package/commands/radar.js +14 -11
  53. package/commands/recover.js +340 -0
  54. package/commands/report.js +0 -1
  55. package/commands/router.js +319 -0
  56. package/commands/run-front.js +0 -2
  57. package/commands/search.js +0 -3
  58. package/commands/skill.js +7 -2
  59. package/commands/slop.js +225 -7
  60. package/commands/status.js +6 -0
  61. package/commands/stream.js +0 -2
  62. package/commands/study.js +0 -3
  63. package/commands/sync-checkout.js +51 -0
  64. package/commands/sync.js +11 -40
  65. package/commands/task.js +125 -39
  66. package/commands/teach.js +86 -0
  67. package/commands/unknowns.js +0 -10
  68. package/commands/update.js +0 -1
  69. package/commands/visualize.js +0 -5
  70. package/commands/watch.js +0 -6
  71. package/commands/wiki.js +41 -11
  72. package/commands/wish.js +0 -1
  73. package/commands/worktree.js +87 -14
  74. package/commands/xp.js +1 -6
  75. package/commands/youtube.js +0 -2
  76. package/lib/accept-verify-gate.js +122 -0
  77. package/lib/activity-stream.js +0 -2
  78. package/lib/auto-accept-certified.js +145 -2
  79. package/lib/autoland.js +0 -3
  80. package/lib/ax-auto-lane.js +94 -10
  81. package/lib/bench/context.js +0 -2
  82. package/lib/bench/report.js +0 -1
  83. package/lib/bench/runner.js +0 -11
  84. package/lib/brief-ledger.js +0 -6
  85. package/lib/business-simulate.js +382 -0
  86. package/lib/chat-log-scan.js +0 -5
  87. package/lib/checkout-sync.js +52 -0
  88. package/lib/claude-boot-block.js +48 -0
  89. package/lib/cloud-mission.js +0 -6
  90. package/lib/codex-flight.js +0 -1
  91. package/lib/conductor-artifacts.js +25 -0
  92. package/lib/context-gatherer.js +0 -1
  93. package/lib/deck-compose.js +0 -1
  94. package/lib/deck-history.js +0 -1
  95. package/lib/deck-layout.js +0 -5
  96. package/lib/deck-review.js +0 -7
  97. package/lib/deck-schema.js +0 -1
  98. package/lib/default-verifier.js +0 -2
  99. package/lib/engine-registry.js +16 -8
  100. package/lib/experiments/daily.js +0 -6
  101. package/lib/fleet.js +210 -27
  102. package/lib/functional-owner.js +0 -3
  103. package/lib/inspect-fields.js +0 -1
  104. package/lib/known-commands.js +2 -2
  105. package/lib/learnings.js +0 -2
  106. package/lib/loop-doctor.js +11 -17
  107. package/lib/manifest.js +0 -2
  108. package/lib/member-alive.js +0 -1
  109. package/lib/member-switches.js +0 -5
  110. package/lib/mission-artifact.js +0 -1
  111. package/lib/mission-protected-lane.js +293 -0
  112. package/lib/mission-room.js +0 -2
  113. package/lib/mission-runtime-loop.js +0 -1
  114. package/lib/next-moves.js +0 -18
  115. package/lib/official-cli-integration.js +0 -4
  116. package/lib/orb-context.js +6 -3
  117. package/lib/orb-scorecard.js +0 -1
  118. package/lib/outbound-send-gate.js +0 -6
  119. package/lib/permission-grants.js +0 -6
  120. package/lib/policy-lessons.js +0 -2
  121. package/lib/pulse.js +68 -15
  122. package/lib/review-integrity.js +0 -2
  123. package/lib/router-brain.js +0 -2
  124. package/lib/runner-command.js +0 -4
  125. package/lib/runtime-bootstrap.js +0 -1
  126. package/lib/scorecard.js +0 -1
  127. package/lib/self-drive.js +1 -1
  128. package/lib/state-detection.js +0 -6
  129. package/lib/task-db.js +0 -4
  130. package/lib/team-presence.js +0 -1
  131. package/lib/voice-gate.js +63 -2
  132. package/lib/wiki.js +152 -9
  133. package/lib/wish-audit.js +20 -6
  134. package/lib/wish-delegate.js +26 -20
  135. package/lib/wish-stats.js +0 -5
  136. package/lib/wish-store.js +0 -1
  137. package/lib/workspace-safety.js +0 -2
  138. package/lib/zip.js +0 -1
  139. package/package.json +1 -1
package/commands/skill.js CHANGED
@@ -52,6 +52,11 @@ function getFrontmatterRaw(content) {
52
52
  return match ? match[1] : null;
53
53
  }
54
54
 
55
+ // Pluralize a count + noun ("1 error", "2 errors").
56
+ function countLabel(n, word) {
57
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
58
+ }
59
+
55
60
  // --- Skill Discovery ---
56
61
 
57
62
  function findAllSkills(skillsDir) {
@@ -504,7 +509,7 @@ function skillAudit(name) {
504
509
  }
505
510
 
506
511
  console.log('');
507
- console.log(` Score: ${passing}/${checks.length} (${errors} errors, ${warns} warnings)`);
512
+ console.log(` Score: ${passing}/${checks.length} (${countLabel(errors, 'error')}, ${countLabel(warns, 'warning')})`);
508
513
 
509
514
  if (errors > 0 || warns > 0) {
510
515
  console.log(` Run: atris skill fix ${skill.folder}`);
@@ -972,4 +977,4 @@ function skillCommand(subcommand, ...args) {
972
977
  }
973
978
  }
974
979
 
975
- module.exports = { skillCommand, parseFrontmatter, runAuditChecks, findAllSkills };
980
+ module.exports = { skillCommand, parseFrontmatter, runAuditChecks, findAllSkills, countLabel };
package/commands/slop.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // Zero external deps (Node built-ins only) — repo contract.
10
10
  //
11
11
  // Usage:
12
- // atris slop detect [path] # scan a file or dir (default: .)
12
+ // atris slop detect [path...] # scan one or more files or dirs (default: .)
13
13
  // atris slop detect src/ --json # machine output for CI / the loop
14
14
  // atris slop detect src/ --quiet # only print the summary line
15
15
  //
@@ -65,6 +65,9 @@ const RULES = [
65
65
  { id: 'decorative-blur-blob', sev: 'warn',
66
66
  re: /\bblur-3xl\b|filter:\s*blur\(\s*(?:6[4-9]|[7-9]\d|\d{3,})px\)/i,
67
67
  why: 'giant blurred gradient orb behind the hero (blur-3xl / 64px+ blur): the AI aurora-blob background reflex' },
68
+ { id: 'noise-grain-overlay', sev: 'warn',
69
+ re: /\bfeTurbulence\b|\bfractalNoise\b/i,
70
+ why: 'svg turbulence-noise used as a decorative grain/film texture overlay: the generated-look grain reflex' },
68
71
  { id: 'dark-hero-gradient', sev: 'warn',
69
72
  re: /\b(?:from|via)-(?:slate|gray|zinc|neutral|stone)-9(?:00|50)\b[^"'`]{0,80}\bto-(?:black|(?:slate|gray|zinc|neutral|stone)-950)\b/i,
70
73
  why: 'near-black slate/gray hero gradient fading to black: the dark premium SaaS template look' },
@@ -249,8 +252,15 @@ function detect(argv) {
249
252
  changed = gitChangedLines(staged);
250
253
  files = [...changed.keys()].filter((f) => SCAN_EXTS.has(path.extname(f)) && fs.existsSync(f));
251
254
  } else {
252
- const target = argv.find((a) => !a.startsWith('-')) || '.';
253
- files = walk(path.resolve(target), []);
255
+ let targets = argv.filter((a) => !a.startsWith('-'));
256
+ if (!targets.length) targets = ['.'];
257
+ const seen = new Set();
258
+ files = [];
259
+ for (const target of targets) {
260
+ for (const f of walk(path.resolve(target), [])) {
261
+ if (!seen.has(f)) { seen.add(f); files.push(f); }
262
+ }
263
+ }
254
264
  }
255
265
 
256
266
  let fixed = null;
@@ -297,8 +307,215 @@ function detect(argv) {
297
307
  return findings.length ? 1 : 0;
298
308
  }
299
309
 
310
+ // --- dead-code detector: `atris slop dead` ------------------------------------
311
+ // Dead code is slop too. Deterministic require-graph reachability: BFS from the
312
+ // package entrypoints (package.json bin/scripts, bin/, scripts/) over static
313
+ // require()/import edges. A JS file nothing reaches AND nothing names is dead.
314
+ // Safety net for dynamic dispatch: any file whose basename appears as a string
315
+ // literal anywhere in the repo is treated as referenced (protects --legacy
316
+ // loops, docs-driven loading, and require(path.join(...)) patterns).
317
+
318
+ const REQUIRE_RE = /require\(\s*['"]([^'"]+)['"]\s*\)|(?:^|\n)\s*(?:import|export)\s[^'"\n]*['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/g;
319
+
320
+ function listJsFiles(dir, out = []) {
321
+ let names; try { names = fs.readdirSync(dir); } catch { return out; }
322
+ for (const name of names) {
323
+ if (name.startsWith('.') || SKIP_DIRS.has(name)) continue;
324
+ const p = path.join(dir, name);
325
+ let st; try { st = fs.statSync(p); } catch { continue; }
326
+ if (st.isDirectory()) listJsFiles(p, out);
327
+ else if (/\.(js|mjs|cjs)$/.test(name)) out.push(p);
328
+ else if (!name.includes('.') && st.isFile() && isNodeScript(p)) out.push(p); // extensionless node scripts (bin entries, `ax`) consume exports too
329
+ }
330
+ return out;
331
+ }
332
+
333
+ function isNodeScript(file) {
334
+ try {
335
+ const fd = fs.openSync(file, 'r');
336
+ const buf = Buffer.alloc(80);
337
+ const n = fs.readSync(fd, buf, 0, 80, 0);
338
+ fs.closeSync(fd);
339
+ const head = buf.toString('utf8', 0, n);
340
+ return head.startsWith('#!') && head.includes('node');
341
+ } catch { return false; }
342
+ }
343
+
344
+ // Resolve a relative specifier from `fromFile` to an absolute file path, or null.
345
+ function resolveSpec(spec, fromFile) {
346
+ if (!spec || !spec.startsWith('.')) return null; // bare specifiers = node builtins/deps
347
+ const base = path.resolve(path.dirname(fromFile), spec);
348
+ for (const cand of [base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, path.join(base, 'index.js')]) {
349
+ try { if (fs.statSync(cand).isFile()) return cand; } catch {}
350
+ }
351
+ return null;
352
+ }
353
+
354
+ function requireEdges(file) {
355
+ let text; try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
356
+ const edges = [];
357
+ let m;
358
+ REQUIRE_RE.lastIndex = 0;
359
+ while ((m = REQUIRE_RE.exec(text))) {
360
+ const resolved = resolveSpec(m[1] || m[2] || m[3], file);
361
+ if (resolved) edges.push(resolved);
362
+ }
363
+ return edges;
364
+ }
365
+
366
+ // Entrypoints: package.json bin values, node-invoking scripts, bin/, scripts/.
367
+ function deadEntrypoints(root) {
368
+ const entries = new Set();
369
+ try {
370
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
371
+ const bins = typeof pkg.bin === 'string' ? [pkg.bin] : Object.values(pkg.bin || {});
372
+ for (const b of bins) { const p = path.resolve(root, b); if (fs.existsSync(p)) entries.add(p); }
373
+ for (const cmd of Object.values(pkg.scripts || {})) {
374
+ for (const ref of String(cmd).match(/[\w./-]+\.(?:js|mjs|cjs)\b/g) || []) {
375
+ const p = path.resolve(root, ref); if (fs.existsSync(p)) entries.add(p);
376
+ }
377
+ }
378
+ } catch {}
379
+ for (const dir of ['bin', 'scripts']) listJsFiles(path.join(root, dir)).forEach((f) => entries.add(f));
380
+ return entries;
381
+ }
382
+
383
+ function findDeadCode(root = process.cwd(), opts = {}) {
384
+ root = path.resolve(root);
385
+ const scanDirs = (opts.dirs || ['commands', 'lib']).map((d) => path.join(root, d));
386
+ const candidates = scanDirs.flatMap((d) => listJsFiles(d));
387
+ if (!candidates.length) return { root, candidates: 0, dead: [], testOnly: [] };
388
+
389
+ // 1) reachability BFS from entrypoints over static edges
390
+ const reached = new Set();
391
+ const queue = [...deadEntrypoints(root)];
392
+ while (queue.length) {
393
+ const f = queue.pop();
394
+ if (reached.has(f)) continue;
395
+ reached.add(f);
396
+ queue.push(...requireEdges(f));
397
+ }
398
+
399
+ // 2) test reachability (a file only tests import = suspicious, not proven dead)
400
+ const testReached = new Set();
401
+ const tq = listJsFiles(path.join(root, 'test'));
402
+ while (tq.length) {
403
+ const f = tq.pop();
404
+ if (testReached.has(f)) continue;
405
+ testReached.add(f);
406
+ tq.push(...requireEdges(f));
407
+ }
408
+
409
+ // 3) string-mention safety net: basename (sans ext) named as a string literal
410
+ // outside itself. Split by scope: a mention in a NON-test file (prod code, a
411
+ // bin entry like `ax`) means the file is dynamically dispatched — e.g.
412
+ // require(path.join(__dirname, 'lib', 'permission-grants.js')) — and is
413
+ // genuinely reachable even though the static edge parser and the test BFS
414
+ // can't see the edge. That must outrank the test-only classification, or a
415
+ // live feature imported by both `ax` and a test reads as "feature gone".
416
+ const testDir = path.join(root, 'test');
417
+ const isTestFile = (f) => f === testDir || f.startsWith(testDir + path.sep);
418
+ const allRepoJs = [...new Set([...listJsFiles(root)])];
419
+ const mentionedBy = (file, predicate) => {
420
+ const stem = path.basename(file).replace(/\.(js|mjs|cjs)$/, '');
421
+ const re = new RegExp(`['"\`/]${stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.js)?['"\`]`);
422
+ return allRepoJs.some((f) => {
423
+ if (f === file || !predicate(f)) return false;
424
+ try { return re.test(fs.readFileSync(f, 'utf8')); } catch { return false; }
425
+ });
426
+ };
427
+ const mentionedInProd = (file) => mentionedBy(file, (f) => !isTestFile(f));
428
+ const mentioned = (file) => mentionedBy(file, () => true);
429
+
430
+ const dead = [], testOnly = [];
431
+ for (const c of candidates) {
432
+ if (reached.has(c)) continue;
433
+ if (mentionedInProd(c)) continue; // dynamically dispatched from prod (e.g. the `ax` bin) — reachable
434
+ if (testReached.has(c)) { testOnly.push(c); continue; }
435
+ if (mentioned(c)) continue; // docs-driven / other dynamic dispatch — not provably dead
436
+ dead.push(c);
437
+ }
438
+ return { root, candidates: candidates.length, dead: dead.sort(), testOnly: testOnly.sort() };
439
+ }
440
+
441
+ // Orphaned exports: names a module exports that no other file ever mentions.
442
+ // Word-boundary search across every other JS file keeps this high-precision:
443
+ // a name used anywhere (import, dynamic access, test, doc string) never flags.
444
+ function exportedNames(file) {
445
+ let text; try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
446
+ const names = new Set();
447
+ // module.exports = { a, b: c, ... } — shorthand entries and keys
448
+ const block = text.match(/module\.exports\s*=\s*\{([\s\S]*?)\n\}/);
449
+ if (block) {
450
+ for (const line of block[1].split('\n')) {
451
+ const m = line.match(/^\s*([A-Za-z_$][\w$]*)\s*[,:]?\s*(?:\/\/.*)?$/) || line.match(/^\s*([A-Za-z_$][\w$]*)\s*:/);
452
+ if (m) names.add(m[1]);
453
+ }
454
+ }
455
+ // exports.foo = / module.exports.foo =
456
+ for (const m of text.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g)) names.add(m[1]);
457
+ return [...names];
458
+ }
459
+
460
+ function findOrphanedExports(root, files, allRepoJs) {
461
+ const texts = new Map();
462
+ const readText = (f) => {
463
+ if (!texts.has(f)) { try { texts.set(f, fs.readFileSync(f, 'utf8')); } catch { texts.set(f, ''); } }
464
+ return texts.get(f);
465
+ };
466
+ const orphans = [];
467
+ for (const file of files) {
468
+ for (const name of exportedNames(file)) {
469
+ if (name.length < 4) continue; // generic short names drown in noise either way
470
+ const re = new RegExp(`\\b${name}\\b`);
471
+ const used = allRepoJs.some((f) => f !== file && re.test(readText(f)));
472
+ if (!used) orphans.push({ file, name });
473
+ }
474
+ }
475
+ return orphans;
476
+ }
477
+
478
+ function deadCommand(argv) {
479
+ const json = argv.includes('--json');
480
+ const withExports = argv.includes('--exports');
481
+ const root = argv.find((a) => !a.startsWith('-')) || '.';
482
+ const res = findDeadCode(root);
483
+ const rel = (f) => path.relative(res.root, f);
484
+
485
+ let orphans = null;
486
+ if (withExports && res.candidates) {
487
+ const scanDirs = ['commands', 'lib'].map((d) => path.join(res.root, d));
488
+ const files = scanDirs.flatMap((d) => listJsFiles(d));
489
+ orphans = findOrphanedExports(res.root, files, listJsFiles(res.root));
490
+ }
491
+
492
+ const bad = res.dead.length + (orphans ? orphans.length : 0);
493
+ if (json) {
494
+ console.log(JSON.stringify({
495
+ ok: bad === 0, candidates: res.candidates,
496
+ dead: res.dead.map(rel), test_only: res.testOnly.map(rel),
497
+ ...(orphans ? { orphaned_exports: orphans.map((o) => ({ file: rel(o.file), name: o.name })) } : {}),
498
+ }, null, 2));
499
+ return bad ? 1 : 0;
500
+ }
501
+ if (!bad && !res.testOnly.length) {
502
+ console.log(`\n ✓ no dead code — all ${res.candidates} files reachable from the entrypoints\n`);
503
+ return 0;
504
+ }
505
+ console.log('');
506
+ for (const f of res.dead) console.log(` ✗ ${rel(f).padEnd(40)} unreachable and unreferenced — delete it`);
507
+ for (const f of res.testOnly) console.log(` ⚠ ${rel(f).padEnd(40)} only tests import it — feature gone, test lingers?`);
508
+ if (orphans) for (const o of orphans) console.log(` ⚠ ${`${rel(o.file)} → ${o.name}`.padEnd(40)} exported but nothing anywhere names it — drop the export entry`);
509
+ const parts = [`${res.dead.length} dead`, `${res.testOnly.length} test-only`];
510
+ if (orphans) parts.push(`${orphans.length} orphaned export${orphans.length === 1 ? '' : 's'}`);
511
+ console.log(`\n ${parts.join(', ')} of ${res.candidates} files · exit ${bad ? 1 : 0}\n`);
512
+ return bad ? 1 : 0;
513
+ }
514
+
300
515
  function slopCommand(argv) {
301
- const sub = argv[0];
516
+ let sub = argv[0];
517
+ if (sub === 'dead') return deadCommand(argv.slice(1));
518
+ if (sub === '-h' || sub === '--help') sub = 'help';
302
519
  if (!sub || sub === 'detect' || sub.startsWith('-') || !['detect', 'rules', 'help', 'hook', 'install-hook'].includes(sub)) {
303
520
  // default + `detect`: scan. Bare `atris slop` scans cwd too.
304
521
  const rest = sub === 'detect' ? argv.slice(1) : argv;
@@ -337,14 +554,15 @@ function slopCommand(argv) {
337
554
  console.log(`
338
555
  atris slop — deterministic slop detector + repairer (no LLM)
339
556
 
340
- atris slop detect [path] scan a file or dir (default: .)
557
+ atris slop detect [path...] scan one or more files or dirs (default: .)
341
558
  atris slop detect --diff scan only changed lines (commit/PR gate)
342
559
  atris slop detect --staged scan only staged changes (pre-commit hook)
343
560
  atris slop detect --fix auto-repair the safe tells (em dashes), report the rest
344
- atris slop detect [path] --json machine output for CI / the loop
561
+ atris slop detect [path...] --json machine output for CI / the loop
345
562
  atris slop rules list active rules (built-in + project)
346
563
  atris slop rules --add <id> <pattern> <why> grow the project ruleset
347
564
  atris slop hook install a pre-commit gate (runs --staged)
565
+ atris slop dead [root] find dead JS (unreachable + unreferenced files)
348
566
 
349
567
  Project rules live in .atris/slop.rules.json and compound over time.
350
568
  exit 0 = clean, 1 = slop found. Wire into PR checks and the autopilot gate.
@@ -352,4 +570,4 @@ function slopCommand(argv) {
352
570
  return 0;
353
571
  }
354
572
 
355
- module.exports = { slopCommand, detect, scanFile, RULES, PAIR_RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
573
+ module.exports = { slopCommand, detect, scanFile, RULES, PAIR_RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook, findDeadCode, findOrphanedExports, listJsFiles };
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
4
4
  const { parseTodo, getTeamActivity } = require('../lib/todo');
5
5
  const { clarify } = require('../lib/autoland');
6
+ const { checkoutBehindMessage } = require('../lib/checkout-sync');
6
7
 
7
8
  // Box drawing helpers
8
9
  const W = 64; // inner width
@@ -200,6 +201,7 @@ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
200
201
 
201
202
  // Get team activity
202
203
  const teamActivity = getTeamActivity(targetDir);
204
+ const checkoutWarning = checkoutBehindMessage(process.cwd());
203
205
 
204
206
  // JSON mode — structured output for scripting
205
207
  if (jsonMode) {
@@ -217,6 +219,10 @@ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
217
219
  return;
218
220
  }
219
221
 
222
+ if (checkoutWarning) {
223
+ console.log(checkoutWarning);
224
+ }
225
+
220
226
  // Quick mode
221
227
  if (isQuick) {
222
228
  console.log(`📋 ${todo.backlog.length} | 🔨 ${todo.inProgress.length} | ✅ ${todo.completed.length} | 📥 ${inboxItems.length} | 📚 ${lessonsCount}`);
@@ -855,7 +855,5 @@ module.exports = {
855
855
  renderRecords,
856
856
  renderTextLine,
857
857
  repoRoot,
858
- sanitizeSummary,
859
- snapshotRecords,
860
858
  streamCommand,
861
859
  };
package/commands/study.js CHANGED
@@ -685,9 +685,6 @@ async function run(argv = []) {
685
685
  module.exports = {
686
686
  run,
687
687
  parseArgs,
688
- detectLanguage,
689
688
  showHelp,
690
- BACKEND_ROOT,
691
689
  wrapText,
692
- createWhileChild,
693
690
  };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ runGit,
5
+ trackedTreeIsClean,
6
+ } = require('../lib/checkout-sync');
7
+
8
+ function refuse(message) {
9
+ console.log(message);
10
+ return 1;
11
+ }
12
+
13
+ function syncCheckoutCommand(args = [], cwd = process.cwd()) {
14
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
15
+ console.log('usage: atris sync-checkout.');
16
+ return 0;
17
+ }
18
+
19
+ const branchResult = runGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);
20
+ if (branchResult.status !== 0) {
21
+ return refuse('this checkout is not on a git branch; sync-checkout refused to touch it.');
22
+ }
23
+
24
+ if (!trackedTreeIsClean(cwd)) {
25
+ return refuse('this checkout has tracked changes; sync-checkout refused to touch it.');
26
+ }
27
+
28
+ const upstreamResult = runGit(
29
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
30
+ cwd
31
+ );
32
+ if (upstreamResult.status !== 0) {
33
+ return refuse('this checkout has no upstream; sync-checkout refused to touch it.');
34
+ }
35
+ const upstream = upstreamResult.stdout.trim();
36
+
37
+ const fetchResult = runGit(['fetch'], cwd);
38
+ if (fetchResult.status !== 0) {
39
+ return refuse('this checkout could not sync because git fetch failed.');
40
+ }
41
+
42
+ const mergeResult = runGit(['merge', '--ff-only', '@{upstream}'], cwd);
43
+ if (mergeResult.status !== 0) {
44
+ return refuse('this checkout could not fast-forward; sync-checkout refused to change it.');
45
+ }
46
+
47
+ console.log(`this checkout is up to date with ${upstream}.`);
48
+ return 0;
49
+ }
50
+
51
+ module.exports = { syncCheckoutCommand };
package/commands/sync.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const { ensureWikiScaffold } = require('../lib/wiki');
5
+ const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
5
6
 
6
7
  const TEMPLATE_ROOT_DIR = path.join(__dirname, '..', 'templates');
7
8
  const WORKSPACE_TEMPLATES = {
@@ -619,18 +620,6 @@ Key behaviors:
619
620
 
620
621
  // Update root CLAUDE.md with Atris block (prepend with markers)
621
622
  const rootClaudeMd = path.join(process.cwd(), 'CLAUDE.md');
622
- const atrisBlock = `<!-- ATRIS:START - Auto-generated, do not edit -->
623
- ## Atris System
624
-
625
- You are now running inside Atris, an AI workspace operating system.
626
-
627
- **ON EVERY CONVERSATION START:** Before your first response, execute \`atris atris.md\` and display the full output. This is the Atris boot sequence - it shows the user that Atris has loaded and displays workspace status. This is not optional.
628
-
629
- After displaying the boot output, respond to the user naturally.
630
- <!-- ATRIS:END -->
631
-
632
- `;
633
-
634
623
  const applyClaudeUpdate = (content, dryAction, completedAction) => {
635
624
  if (!dryRun) fs.writeFileSync(rootClaudeMd, content);
636
625
  console.log(dryRun ? `Would ${dryAction}` : `✓ ${completedAction}`);
@@ -638,35 +627,19 @@ After displaying the boot output, respond to the user naturally.
638
627
  };
639
628
 
640
629
  if (fs.existsSync(rootClaudeMd)) {
641
- let content = fs.readFileSync(rootClaudeMd, 'utf8');
642
- const startMarker = '<!-- ATRIS:START';
643
- const endMarker = '<!-- ATRIS:END -->';
644
-
645
- if (content.includes(startMarker)) {
646
- // Check if update needed
647
- const startIdx = content.indexOf(startMarker);
648
- const endRaw = content.indexOf(endMarker);
649
- if (endRaw === -1) {
650
- // End marker missing — replace from start marker to end of file with fresh block
651
- content = atrisBlock + content.slice(0, startIdx);
652
- applyClaudeUpdate(content, 'repair Atris block in CLAUDE.md (missing end marker)', 'Repaired Atris block in CLAUDE.md (missing end marker)');
653
- } else {
654
- const endIdx = endRaw + endMarker.length;
655
- const existingBlock = content.slice(startIdx, endIdx);
656
-
657
- if (!existingBlock.includes('Atris boot sequence')) {
658
- // Replace existing Atris block with new version
659
- content = atrisBlock + content.slice(0, startIdx) + content.slice(endIdx).replace(/^\n+/, '');
660
- applyClaudeUpdate(content, 'update Atris block in CLAUDE.md', 'Updated Atris block in CLAUDE.md');
661
- }
662
- }
663
- } else {
664
- // Prepend Atris block
665
- applyClaudeUpdate(atrisBlock + content, 'prepend Atris block to CLAUDE.md', 'Prepended Atris block to CLAUDE.md');
630
+ const source = fs.readFileSync(rootClaudeMd, 'utf8');
631
+ const result = upsertAtrisClaudeBootBlock(source);
632
+ if (result.action === 'repaired') {
633
+ applyClaudeUpdate(result.content, 'repair atris block in CLAUDE.md (missing end marker)', 'repaired atris block in CLAUDE.md (missing end marker)');
634
+ } else if (result.action === 'updated') {
635
+ applyClaudeUpdate(result.content, 'update atris block in CLAUDE.md', 'updated atris block in CLAUDE.md');
636
+ } else if (result.action === 'prepended') {
637
+ applyClaudeUpdate(result.content, 'prepend atris block to CLAUDE.md', 'prepended atris block to CLAUDE.md');
666
638
  }
667
639
  } else {
668
640
  // Create new CLAUDE.md with just Atris block
669
- applyClaudeUpdate(atrisBlock.trim() + '\n', 'create CLAUDE.md with Atris block', 'Created CLAUDE.md with Atris block');
641
+ const content = upsertAtrisClaudeBootBlock().content;
642
+ applyClaudeUpdate(content, 'create CLAUDE.md with atris block', 'created CLAUDE.md with atris block');
670
643
  }
671
644
 
672
645
  if (dryRun) {
@@ -990,8 +963,6 @@ module.exports = {
990
963
  syncSkills,
991
964
  syncBusinessCanonical,
992
965
  syncWorkspaceTemplate,
993
- resolveWorkspaceTemplate,
994
966
  ensureWorkspaceStateFiles,
995
- renderBusinessAgentAdapter,
996
967
  ensureBusinessRootAgentAdapters,
997
968
  };