atris 3.37.1 → 3.40.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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
package/commands/site.js CHANGED
@@ -4,21 +4,30 @@
4
4
  // atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T] [--serve]
5
5
 
6
6
  const { buildSite, serveSite } = require('../lib/site');
7
+ const { hasFlag } = require('../lib/arg-parser');
7
8
 
8
9
  function flag(argv, name) { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; }
9
- function hasFlag(argv, name) { return argv.includes(name); }
10
10
 
11
11
  async function run(argv) {
12
+ if (argv[0] === 'deploy') {
13
+ return require('./site-deploy').run(argv.slice(1));
14
+ }
15
+
12
16
  const input = argv.find((a) => !a.startsWith('-'));
13
17
  if (!input || input === 'help' || hasFlag(argv, '--help')) {
14
18
  console.log(`
15
- atris site — a beautiful static site from a folder of markdown
19
+ atris site: build markdown sites or deploy web folders
16
20
 
17
21
  atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T]
18
22
  atris site atris/wiki --title "Atris Wiki" --serve
23
+ atris site deploy <dir> --name <slug> [--spa] [--dry-run]
24
+ atris site deploy <dir> --fullstack --name <slug> [--dry-run]
19
25
 
20
26
  Each .md becomes a page; an index links them all. Same anti-slop design system,
21
27
  semantic data-atris-block sections, ready for the web app. --serve previews it.
28
+
29
+ deploy publishes web folders to <slug>.atris.ai. --fullstack deploys a node
30
+ server with its own render service.
22
31
  `);
23
32
  return input === 'help' || hasFlag(argv, '--help') ? 0 : 2;
24
33
  }
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,227 @@ 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
+ // Token index instead of a per-name regex pass over every file: one read +
467
+ // tokenize per file, then O(1) name lookups. The regex version took nearly a
468
+ // minute on this repo, which kept this detector out of the landing gate.
469
+ // Identifiers split on `$` so `foo` still matches inside `foo$bar`, matching
470
+ // the old \b semantics; the rare `$`-carrying name falls back to the regex.
471
+ const tokens = new Map();
472
+ const tokensOf = (f) => {
473
+ if (!tokens.has(f)) tokens.set(f, new Set(readText(f).match(/[A-Za-z0-9_]+/g) || []));
474
+ return tokens.get(f);
475
+ };
476
+ const orphans = [];
477
+ for (const file of files) {
478
+ for (const name of exportedNames(file)) {
479
+ if (name.length < 4) continue; // generic short names drown in noise either way
480
+ const usesRegex = name.includes('$');
481
+ const re = usesRegex ? new RegExp(`\\b${name.replace(/\$/g, '\\$')}\\b`) : null;
482
+ const used = allRepoJs.some((f) => f !== file
483
+ && (usesRegex ? re.test(readText(f)) : tokensOf(f).has(name)));
484
+ if (!used) orphans.push({ file, name });
485
+ }
486
+ }
487
+ return orphans;
488
+ }
489
+
490
+ function deadCommand(argv) {
491
+ const json = argv.includes('--json');
492
+ const withExports = argv.includes('--exports');
493
+ const root = argv.find((a) => !a.startsWith('-')) || '.';
494
+ const res = findDeadCode(root);
495
+ const rel = (f) => path.relative(res.root, f);
496
+
497
+ let orphans = null;
498
+ if (withExports && res.candidates) {
499
+ const scanDirs = ['commands', 'lib'].map((d) => path.join(res.root, d));
500
+ const files = scanDirs.flatMap((d) => listJsFiles(d));
501
+ orphans = findOrphanedExports(res.root, files, listJsFiles(res.root));
502
+ }
503
+
504
+ const bad = res.dead.length + (orphans ? orphans.length : 0);
505
+ if (json) {
506
+ console.log(JSON.stringify({
507
+ ok: bad === 0, candidates: res.candidates,
508
+ dead: res.dead.map(rel), test_only: res.testOnly.map(rel),
509
+ ...(orphans ? { orphaned_exports: orphans.map((o) => ({ file: rel(o.file), name: o.name })) } : {}),
510
+ }, null, 2));
511
+ return bad ? 1 : 0;
512
+ }
513
+ if (!bad && !res.testOnly.length) {
514
+ console.log(`\n ✓ no dead code — all ${res.candidates} files reachable from the entrypoints\n`);
515
+ return 0;
516
+ }
517
+ console.log('');
518
+ for (const f of res.dead) console.log(` ✗ ${rel(f).padEnd(40)} unreachable and unreferenced — delete it`);
519
+ for (const f of res.testOnly) console.log(` ⚠ ${rel(f).padEnd(40)} only tests import it — feature gone, test lingers?`);
520
+ 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`);
521
+ const parts = [`${res.dead.length} dead`, `${res.testOnly.length} test-only`];
522
+ if (orphans) parts.push(`${orphans.length} orphaned export${orphans.length === 1 ? '' : 's'}`);
523
+ console.log(`\n ${parts.join(', ')} of ${res.candidates} files · exit ${bad ? 1 : 0}\n`);
524
+ return bad ? 1 : 0;
525
+ }
526
+
300
527
  function slopCommand(argv) {
301
- const sub = argv[0];
528
+ let sub = argv[0];
529
+ if (sub === 'dead') return deadCommand(argv.slice(1));
530
+ if (sub === '-h' || sub === '--help') sub = 'help';
302
531
  if (!sub || sub === 'detect' || sub.startsWith('-') || !['detect', 'rules', 'help', 'hook', 'install-hook'].includes(sub)) {
303
532
  // default + `detect`: scan. Bare `atris slop` scans cwd too.
304
533
  const rest = sub === 'detect' ? argv.slice(1) : argv;
@@ -337,14 +566,15 @@ function slopCommand(argv) {
337
566
  console.log(`
338
567
  atris slop — deterministic slop detector + repairer (no LLM)
339
568
 
340
- atris slop detect [path] scan a file or dir (default: .)
569
+ atris slop detect [path...] scan one or more files or dirs (default: .)
341
570
  atris slop detect --diff scan only changed lines (commit/PR gate)
342
571
  atris slop detect --staged scan only staged changes (pre-commit hook)
343
572
  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
573
+ atris slop detect [path...] --json machine output for CI / the loop
345
574
  atris slop rules list active rules (built-in + project)
346
575
  atris slop rules --add <id> <pattern> <why> grow the project ruleset
347
576
  atris slop hook install a pre-commit gate (runs --staged)
577
+ atris slop dead [root] find dead JS (unreachable + unreferenced files)
348
578
 
349
579
  Project rules live in .atris/slop.rules.json and compound over time.
350
580
  exit 0 = clean, 1 = slop found. Wire into PR checks and the autopilot gate.
@@ -352,4 +582,4 @@ function slopCommand(argv) {
352
582
  return 0;
353
583
  }
354
584
 
355
- module.exports = { slopCommand, detect, scanFile, RULES, PAIR_RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
585
+ 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}`);
@@ -2,8 +2,8 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { spawnSync } = require('child_process');
6
-
5
+ const { hasFlag, readFlag } = require('../lib/arg-parser');
6
+ const { runGit: spawnGit } = require('../lib/git-spawn');
7
7
  const { collectBoard } = require('./land');
8
8
  const { listWorktrees } = require('./worktree');
9
9
 
@@ -20,25 +20,11 @@ function defaultDeps() {
20
20
  readdirSync: fs.readdirSync,
21
21
  statSync: fs.statSync,
22
22
  watch: fs.watch,
23
- spawnSync,
23
+ runGit: (root, args) => spawnGit(args, { cwd: root, check: false }),
24
24
  now: () => Date.now(),
25
25
  };
26
26
  }
27
27
 
28
- function hasFlag(args, name) {
29
- return args.includes(name);
30
- }
31
-
32
- function readFlag(args, name, fallback = '') {
33
- const prefix = `${name}=`;
34
- for (let i = 0; i < args.length; i += 1) {
35
- const arg = String(args[i] || '');
36
- if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return String(args[i + 1]);
37
- if (arg.startsWith(prefix)) return arg.slice(prefix.length);
38
- }
39
- return fallback;
40
- }
41
-
42
28
  function timestampMs(value) {
43
29
  if (value == null || value === '') return 0;
44
30
  if (typeof value === 'number') {
@@ -102,7 +88,7 @@ function isDirectory(file, deps) {
102
88
 
103
89
  function runGit(root, args, deps) {
104
90
  try {
105
- const result = deps.spawnSync('git', args, { cwd: root, encoding: 'utf8' });
91
+ const result = deps.runGit(root, args);
106
92
  return result && typeof result.status === 'number' ? result : { status: 1, stdout: '', stderr: '' };
107
93
  } catch {
108
94
  return { status: 1, stdout: '', stderr: '' };
@@ -855,7 +841,5 @@ module.exports = {
855
841
  renderRecords,
856
842
  renderTextLine,
857
843
  repoRoot,
858
- sanitizeSummary,
859
- snapshotRecords,
860
844
  streamCommand,
861
845
  };
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
  };