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.
- package/README.md +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +125 -39
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +94 -10
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +0 -1
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-protected-lane.js +293 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +0 -1
- 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}
|
|
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]
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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]
|
|
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 };
|
package/commands/status.js
CHANGED
|
@@ -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}`);
|
package/commands/stream.js
CHANGED
package/commands/study.js
CHANGED
|
@@ -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
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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
|
};
|