atris 3.34.0 → 3.36.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 (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { loadManifest, saveManifest } = require('../lib/manifest');
5
5
  const { removeBaseContents, writeBaseContents } = require('../lib/company-brain-sync');
6
+ const { collectCloudOrphans } = require('../commands/cloud');
6
7
 
7
8
  const WATCH_IGNORED_DIRS = new Set([
8
9
  '.git', '.atris', '.claude', '.cursor', '.next', '.cache',
@@ -31,6 +32,8 @@ function parseBusinessSyncArgs(args = []) {
31
32
  const resolveFlag = parseFlagValue(args, '--resolve', null);
32
33
  const resolve = resolveFlag || (resolveIdx !== -1 ? positional[resolveIdx + 1] : null);
33
34
  const resolvePath = parseFlagValue(args, '--path', parseFlagValue(args, '--file', null));
35
+ const takeRaw = parseFlagValue(args, '--take', null);
36
+ const take = takeRaw ? String(takeRaw).trim().toLowerCase() : null;
34
37
  const commandWords = new Set(['status', 'doctor', 'review', 'resolve', 'local', 'cloud', 'both', 'merge']);
35
38
  const slug = positional.find((arg) => !commandWords.has(arg)) || null;
36
39
  const dryRun = args.includes('--dry-run');
@@ -39,9 +42,10 @@ function parseBusinessSyncArgs(args = []) {
39
42
  const watch = args.includes('--watch');
40
43
  const intervalSec = Number.parseInt(parseFlagValue(args, '--interval', '60'), 10);
41
44
  const debounceSec = Number.parseInt(parseFlagValue(args, '--debounce', '5'), 10);
45
+ const showOrphans = args.includes('--show-orphans');
42
46
  const help = args.includes('--help') || args.includes('-h') || positional[0] === 'help';
43
47
 
44
- return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, resolvePath, help };
48
+ return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, resolvePath, take, showOrphans, help };
45
49
  }
46
50
 
47
51
  function readBusinessSlug(cwd = process.cwd()) {
@@ -322,6 +326,20 @@ function renderLocalSyncStatus(status) {
322
326
  } else {
323
327
  lines.push(' warnings: none');
324
328
  }
329
+ if (status.orphanError !== undefined) {
330
+ lines.push(` orphans: ${status.orphanError}`);
331
+ } else if (status.orphanPaths !== undefined) {
332
+ if (status.orphanPaths.length === 0) {
333
+ lines.push(' orphans: none');
334
+ } else {
335
+ const orphanLabel = status.orphanPaths.length === 1 ? 'file' : 'files';
336
+ lines.push(` orphans: ${status.orphanPaths.length} cloud ${orphanLabel} not present locally`);
337
+ status.orphanPaths.slice(0, 20).forEach((p) => lines.push(` - ${p.replace(/^\//, '')}`));
338
+ if (status.orphanPaths.length > 20) {
339
+ lines.push(` ... +${status.orphanPaths.length - 20} more`);
340
+ }
341
+ }
342
+ }
325
343
  if (status.heartbeat && status.heartbeat.updated_at) {
326
344
  lines.push(` watcher: last heartbeat ${status.heartbeat.updated_at} (${status.heartbeat.state || 'unknown'})`);
327
345
  } else {
@@ -334,15 +352,18 @@ function renderLocalSyncStatus(status) {
334
352
 
335
353
  function renderBusinessSyncHelp() {
336
354
  return [
337
- 'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--review] [--resolve local|cloud|both|merge] [--path <file>] [--timeout 120]',
355
+ 'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--show-orphans] [--review] [--take local|cloud] [--resolve local|cloud|both|merge] [--path <file>] [--timeout 120]',
338
356
  '',
339
357
  'Safe loop:',
340
358
  ' Pull -> Review -> Publish',
341
359
  '',
342
360
  'Commands:',
343
361
  ' atris sync --status Show local sync health',
362
+ ' atris sync --status --show-orphans Show local sync health plus cloud files not present locally',
344
363
  ' atris sync --dry-run Preview pull and publish plans without writing cloud',
345
- ' atris sync --review Read the latest conflict packet',
364
+ ' atris sync --review List conflicts; pick local / cloud / merge per file',
365
+ ' atris sync --review --take local|cloud',
366
+ ' Non-interactive: take local or cloud for every conflict',
346
367
  ' atris sync --resolve cloud|local|merge [--path <file>]',
347
368
  ' atris sync --watch Keep the workspace live with the same safety gates',
348
369
  '',
@@ -353,6 +374,147 @@ function renderBusinessSyncHelp() {
353
374
  ].join('\n');
354
375
  }
355
376
 
377
+ function readTextIfExists(filePath) {
378
+ try {
379
+ return fs.readFileSync(filePath, 'utf8');
380
+ } catch {
381
+ return '';
382
+ }
383
+ }
384
+
385
+ function buildConflictMarkers(localContent = '', remoteContent = '') {
386
+ const local = String(localContent);
387
+ const remote = String(remoteContent);
388
+ const localBody = local.endsWith('\n') || local === '' ? local : `${local}\n`;
389
+ const remoteBody = remote.endsWith('\n') || remote === '' ? remote : `${remote}\n`;
390
+ return `<<<<<<< local\n${localBody}=======\n${remoteBody}>>>>>>> cloud\n`;
391
+ }
392
+
393
+ function listConflictFiles(entries = []) {
394
+ const lines = [];
395
+ const count = entries.length;
396
+ if (count === 0) {
397
+ lines.push('no sync conflicts need review.');
398
+ return `${lines.join('\n')}\n`;
399
+ }
400
+ lines.push(`${count} file${count === 1 ? '' : 's'} conflict`);
401
+ entries.forEach((entry, index) => {
402
+ lines.push(` ${index + 1}. ${entry.targetRel}`);
403
+ });
404
+ lines.push('');
405
+ lines.push('pick local / cloud / merge per file (interactive), or run:');
406
+ lines.push(' atris sync --review --take local');
407
+ lines.push(' atris sync --review --take cloud');
408
+ lines.push('merge writes standard conflict markers into the file (no .remote dump).');
409
+ return `${lines.join('\n')}\n`;
410
+ }
411
+
412
+ function applyReviewChoice(cwd, entry, choice) {
413
+ const strategy = String(choice || '').trim().toLowerCase();
414
+ if (!['local', 'cloud', 'merge'].includes(strategy)) {
415
+ throw new Error(`unknown choice "${choice}". use local, cloud, or merge.`);
416
+ }
417
+
418
+ const targetPath = assertWorkspaceTarget(cwd, entry.targetRel);
419
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
420
+
421
+ if (strategy === 'merge') {
422
+ const localContent = readTextIfExists(entry.localPath);
423
+ const remoteContent = readTextIfExists(entry.remotePath);
424
+ fs.writeFileSync(targetPath, buildConflictMarkers(localContent, remoteContent), 'utf8');
425
+ } else {
426
+ const sourcePath = strategy === 'local' ? entry.localPath : entry.remotePath;
427
+ if (!fs.existsSync(sourcePath)) {
428
+ throw new Error(`missing ${strategy} artifact for ${entry.targetRel}`);
429
+ }
430
+ fs.copyFileSync(sourcePath, targetPath);
431
+ }
432
+
433
+ // Review path never leaves sibling .remote dumps next to workspace files.
434
+ cleanupResolvedConflictSidecars(cwd, entry.targetRel, { keepCloud: false });
435
+ cleanupConflictPacketArtifacts(entry);
436
+ return { path: entry.targetRel, choice: strategy };
437
+ }
438
+
439
+ async function promptReviewChoice(entry, ask) {
440
+ const answer = await ask(`${entry.targetRel}: local / cloud / merge? `);
441
+ const choice = String(answer || '').trim().toLowerCase();
442
+ if (!['local', 'cloud', 'merge'].includes(choice)) {
443
+ throw new Error(`invalid choice "${answer}". use local, cloud, or merge.`);
444
+ }
445
+ return choice;
446
+ }
447
+
448
+ /**
449
+ * S2 conflict handler for `atris sync --review`.
450
+ * Lists each conflicting file, then resolves via interactive prompt or `--take local|cloud`.
451
+ * Merge writes standard conflict markers into the target file (no workspace .remote dump).
452
+ */
453
+ async function runSyncReview(cwd = process.cwd(), options = {}) {
454
+ const take = options.take ? String(options.take).trim().toLowerCase() : null;
455
+ if (take && !['local', 'cloud'].includes(take)) {
456
+ return {
457
+ resolved: [],
458
+ choices: [],
459
+ message: 'use --take local or --take cloud (merge is interactive only).\n',
460
+ exitCode: 1,
461
+ };
462
+ }
463
+
464
+ const wantedTarget = normalizeResolvePath(options.path || options.resolvePath || null);
465
+ const entries = collectConflictResolutionEntries(cwd)
466
+ .filter((entry) => !wantedTarget || normalizeResolvePath(entry.targetRel) === wantedTarget);
467
+
468
+ if (entries.length === 0) {
469
+ // Fall back to packet summary when no resolvable .local artifacts exist.
470
+ return {
471
+ resolved: [],
472
+ choices: [],
473
+ message: renderLatestConflictReview(cwd),
474
+ exitCode: 0,
475
+ };
476
+ }
477
+
478
+ const lines = [];
479
+ lines.push(`${entries.length} file${entries.length === 1 ? '' : 's'} conflict`);
480
+ entries.forEach((entry, index) => {
481
+ lines.push(` ${index + 1}. ${entry.targetRel}`);
482
+ });
483
+
484
+ const interactive = typeof options.ask === 'function';
485
+ if (!take && !interactive) {
486
+ lines.push('');
487
+ lines.push('pick local / cloud / merge per file (interactive), or run:');
488
+ lines.push(' atris sync --review --take local');
489
+ lines.push(' atris sync --review --take cloud');
490
+ lines.push('merge writes standard conflict markers into the file (no .remote dump).');
491
+ lines.push('');
492
+ lines.push(renderLatestConflictReview(cwd).trimEnd());
493
+ lines.push('');
494
+ return { resolved: [], choices: [], message: `${lines.join('\n')}\n`, exitCode: 0 };
495
+ }
496
+
497
+ const choices = [];
498
+ const resolved = [];
499
+ for (const entry of entries) {
500
+ const choice = take || await promptReviewChoice(entry, options.ask);
501
+ const result = applyReviewChoice(cwd, entry, choice);
502
+ choices.push(result);
503
+ resolved.push(result.path);
504
+ lines.push(` -> ${result.path}: ${result.choice}`);
505
+ }
506
+
507
+ const finalizedManifests = cleanupEmptyConflictPackets(cwd, entries);
508
+ lines.push('');
509
+ lines.push(`resolved ${resolved.length} conflict${resolved.length === 1 ? '' : 's'}.`);
510
+ if (finalizedManifests.length) {
511
+ lines.push(`updated sync manifest for ${finalizedManifests.join(', ')}.`);
512
+ }
513
+ lines.push('next: run `atris sync --dry-run` before publishing.');
514
+ lines.push('');
515
+ return { resolved, choices, message: `${lines.join('\n')}\n`, exitCode: 0 };
516
+ }
517
+
356
518
  function renderLatestConflictReview(cwd = process.cwd()) {
357
519
  const summaries = listConflictSummaries(cwd);
358
520
  if (summaries.length === 0) {
@@ -782,7 +944,7 @@ async function runSyncCycle(plan, cwd, options = {}) {
782
944
  }
783
945
  }
784
946
 
785
- async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
947
+ async function businessSync(args = process.argv.slice(3), cwd = process.cwd(), deps = {}) {
786
948
  const options = resolveBusinessSyncOptions(args, cwd);
787
949
 
788
950
  if (options.help) {
@@ -791,12 +953,32 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
791
953
  }
792
954
 
793
955
  if (options.status) {
794
- process.stdout.write(renderLocalSyncStatus(collectLocalSyncStatus(cwd, options)));
956
+ const status = collectLocalSyncStatus(cwd, options);
957
+ if (options.showOrphans) {
958
+ try {
959
+ const { orphanPaths } = await collectCloudOrphans(cwd, options.slug, deps);
960
+ status.orphanPaths = orphanPaths;
961
+ } catch (err) {
962
+ status.orphanError = err.message || String(err);
963
+ }
964
+ }
965
+ process.stdout.write(renderLocalSyncStatus(status));
795
966
  return;
796
967
  }
797
968
 
798
969
  if (options.review) {
799
- process.stdout.write(renderLatestConflictReview(cwd));
970
+ let ask = null;
971
+ if (!options.take && process.stdin.isTTY) {
972
+ const { promptUser } = require('../utils/auth');
973
+ ask = (question) => promptUser(question);
974
+ }
975
+ const result = await runSyncReview(cwd, {
976
+ take: options.take,
977
+ path: options.resolvePath,
978
+ ask,
979
+ });
980
+ process.stdout.write(result.message);
981
+ if (result.exitCode && result.exitCode !== 0) process.exit(result.exitCode);
800
982
  return;
801
983
  }
802
984
 
@@ -894,8 +1076,10 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
894
1076
  }
895
1077
 
896
1078
  module.exports = {
1079
+ applyReviewChoice,
897
1080
  businessSync,
898
1081
  buildBusinessSyncPlan,
1082
+ buildConflictMarkers,
899
1083
  canPreviewPush,
900
1084
  collectBrainSnapshot,
901
1085
  collectWorkspaceSnapshot,
@@ -903,6 +1087,7 @@ module.exports = {
903
1087
  collectLocalSyncStatus,
904
1088
  collectConflictResolutionEntries,
905
1089
  describeWatchFailure,
1090
+ listConflictFiles,
906
1091
  parseBusinessSyncArgs,
907
1092
  readBusinessSlug,
908
1093
  renderLatestConflictReview,
@@ -910,6 +1095,7 @@ module.exports = {
910
1095
  renderBusinessSyncHelp,
911
1096
  resolveLatestConflict,
912
1097
  resolveBusinessSyncOptions,
1098
+ runSyncReview,
913
1099
  safeLineMerge,
914
1100
  safeMarkdownMerge,
915
1101
  shouldIgnoreWatchPath,
@@ -28,6 +28,42 @@ function isHelpToken(arg) {
28
28
  return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
29
29
  }
30
30
 
31
+ const BUSINESS_CREATE_USAGE = 'Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]';
32
+
33
+ function isAccidentalHelpBusiness(value) {
34
+ const raw = String(value || '').trim();
35
+ if (!raw) return false;
36
+ if (isHelpToken(raw)) return true;
37
+ const normalized = slugify(raw);
38
+ if (normalized === 'help') return true;
39
+ if (/^help-\d+$/.test(normalized)) return true;
40
+ return false;
41
+ }
42
+
43
+ function validateBusinessCreateName(name) {
44
+ if (!name || !String(name).trim()) {
45
+ return { ok: false, usage: BUSINESS_CREATE_USAGE };
46
+ }
47
+ if (isHelpToken(name)) {
48
+ return { ok: false, usage: BUSINESS_CREATE_USAGE };
49
+ }
50
+ if (String(name).startsWith('-')) {
51
+ return {
52
+ ok: false,
53
+ usage: BUSINESS_CREATE_USAGE,
54
+ detail: `Refusing to create a business named "${name}" — looks like a flag, not a name.`,
55
+ };
56
+ }
57
+ if (isAccidentalHelpBusiness(name)) {
58
+ return {
59
+ ok: false,
60
+ usage: BUSINESS_CREATE_USAGE,
61
+ detail: `Refusing to create a business named "${name}" — looks like accidental help output, not a real business.`,
62
+ };
63
+ }
64
+ return { ok: true };
65
+ }
66
+
31
67
  function loadBusinesses() {
32
68
  const p = getBusinessConfigPath();
33
69
  if (!fs.existsSync(p)) return {};
@@ -308,14 +344,46 @@ function analyzeBusinessDoctor({ cache = {}, cloudBusinesses = [], folderBinding
308
344
  }
309
345
  }
310
346
 
347
+ const cacheRemovals = [];
348
+ for (const business of cloudBusinesses || []) {
349
+ if (!isAccidentalHelpBusiness(business?.name) && !isAccidentalHelpBusiness(business?.slug)) continue;
350
+ const label = business.slug || business.name || business.id;
351
+ issues.push({
352
+ level: 'warn',
353
+ code: 'accidental-help-business',
354
+ subject: label,
355
+ message: `${label} looks like an accidental help business (April 2026 ghost rows)`,
356
+ fixable: true,
357
+ });
358
+ }
359
+
360
+ for (const [key, entry] of Object.entries(cache || {})) {
361
+ const active = findActiveMatch(key, entry);
362
+ const accidental = isAccidentalHelpBusiness(key)
363
+ || isAccidentalHelpBusiness(entry?.name)
364
+ || isAccidentalHelpBusiness(entry?.slug)
365
+ || (active && (isAccidentalHelpBusiness(active.name) || isAccidentalHelpBusiness(active.slug)));
366
+ if (!accidental) continue;
367
+ issues.push({
368
+ level: 'warn',
369
+ code: 'accidental-help-cache',
370
+ subject: key,
371
+ message: `${key} is cached for an accidental help business`,
372
+ fixable: true,
373
+ });
374
+ if (!cacheRemovals.includes(key)) cacheRemovals.push(key);
375
+ }
376
+
311
377
  return {
312
378
  issues,
313
379
  cacheUpdates,
380
+ cacheRemovals,
314
381
  stats: {
315
382
  cache_entries: Object.keys(cache || {}).length,
316
383
  cloud_active: cloudBusinesses.length,
317
384
  folders: folderBindings.length,
318
385
  fixable_cache_entries: Object.keys(cacheUpdates).length,
386
+ accidental_help_cache_removals: cacheRemovals.length,
319
387
  },
320
388
  };
321
389
  }
@@ -2191,11 +2259,20 @@ async function businessDoctor(...args) {
2191
2259
  });
2192
2260
 
2193
2261
  const cacheUpdateKeys = Object.keys(analysis.cacheUpdates);
2262
+ const cacheRemovalKeys = analysis.cacheRemovals || [];
2194
2263
  let fixed = [];
2195
- if (options.fix && cacheUpdateKeys.length > 0) {
2196
- cache = { ...cache, ...analysis.cacheUpdates };
2264
+ if (options.fix && (cacheUpdateKeys.length > 0 || cacheRemovalKeys.length > 0)) {
2265
+ if (cacheUpdateKeys.length > 0) {
2266
+ cache = { ...cache, ...analysis.cacheUpdates };
2267
+ }
2268
+ for (const key of cacheRemovalKeys) {
2269
+ delete cache[key];
2270
+ fixed.push(`removed:${key}`);
2271
+ }
2272
+ if (cacheUpdateKeys.length > 0) {
2273
+ fixed.push(...cacheUpdateKeys);
2274
+ }
2197
2275
  saveBusinesses(cache);
2198
- fixed = cacheUpdateKeys;
2199
2276
  analysis = analyzeBusinessDoctor({
2200
2277
  cache,
2201
2278
  cloudBusinesses: listResult.data,
@@ -2237,11 +2314,10 @@ async function businessDoctor(...args) {
2237
2314
  }
2238
2315
 
2239
2316
  async function createBusinessInternal(name, flags = [], mode = 'auto') {
2240
- if (!name || isHelpToken(name) || String(name).startsWith('-')) {
2241
- console.error('Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]');
2242
- if (name && String(name).startsWith('-') && !isHelpToken(name)) {
2243
- console.error(`\n Refusing to create a business named "${name}" — looks like a flag, not a name.`);
2244
- }
2317
+ const validation = validateBusinessCreateName(name);
2318
+ if (!validation.ok) {
2319
+ console.error(validation.usage);
2320
+ if (validation.detail) console.error(`\n ${validation.detail}`);
2245
2321
  process.exit(1);
2246
2322
  }
2247
2323
 
@@ -2897,6 +2973,11 @@ async function businessCommand(subcommand, ...args) {
2897
2973
  printBusinessHelp();
2898
2974
  return;
2899
2975
  }
2976
+ const nameTakingSubcommands = new Set(['create', 'new', 'init', 'workspace']);
2977
+ if (nameTakingSubcommands.has(subcommand) && args.some(isHelpToken)) {
2978
+ printBusinessHelp();
2979
+ return;
2980
+ }
2900
2981
  if (args.length > 0 && isHelpToken(args[0]) && subcommand !== 'doctor') {
2901
2982
  printBusinessHelp();
2902
2983
  return;
@@ -2999,6 +3080,8 @@ module.exports = {
2999
3080
  saveBusinesses,
3000
3081
  getBusinessConfigPath,
3001
3082
  businessMatchesSlug,
3083
+ isAccidentalHelpBusiness,
3084
+ validateBusinessCreateName,
3002
3085
  analyzeBusinessDoctor,
3003
3086
  readBusinessFolderBindings,
3004
3087
  createCanonicalBusinessWorkspace,
package/commands/clean.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const os = require('os');
2
3
  const path = require('path');
3
4
  const escapeRegExp = require('../lib/escape-regexp');
4
5
 
@@ -250,7 +251,7 @@ function findStaleTasks(atrisDir) {
250
251
  * Detects both out-of-bounds AND drift (symbol moved to different line)
251
252
  * Returns { healed: number, unhealable: array }
252
253
  */
253
- function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
254
+ function healBrokenMapRefs(cwd, atrisDir, dryRun = false, homeDir = os.homedir()) {
254
255
  const mapFile = path.join(atrisDir, 'MAP.md');
255
256
  if (!fs.existsSync(mapFile)) return { healed: 0, unhealable: [] };
256
257
 
@@ -261,12 +262,12 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
261
262
  // Match both `file.js:123` and `file.js:123-456` with surrounding context
262
263
  // [^\S\n] = horizontal whitespace only (no newlines)
263
264
  // Required delimiter [(,[,—,-] prevents bleeding into adjacent refs on same line
264
- const refPattern = /(`?)([a-zA-Z0-9_\-./\\]+\.(js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-(\d+))?(`?)([^\S\n]*[\(\[—\-][^\S\n]*([^)\]\n]+))?/g;
265
+ const refPattern = /(`?)([a-zA-Z0-9_~\-./\\]+\.(js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-(\d+))?(`?)([^\S\n]*[\(\[—\-][^\S\n]*([^)\]\n]+))?/g;
265
266
 
266
267
  // Function Inventory form: `symbolName()` → `file:line[-line]`
267
268
  // Pre-scan to build a (file:line) → symbol map so refs with the symbol BEFORE them still verify.
268
269
  const preRefSymbols = {};
269
- const preRefPattern = /`([a-zA-Z_][a-zA-Z0-9_]*)\s*\(?\s*\)?`\s*(?:→|->)\s*`([a-zA-Z0-9_\-./\\]+\.(?:js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-\d+)?`/g;
270
+ const preRefPattern = /`([a-zA-Z_][a-zA-Z0-9_]*)\s*\(?\s*\)?`\s*(?:→|->)\s*`([a-zA-Z0-9_~\-./\\]+\.(?:js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-\d+)?`/g;
270
271
  let preMatch;
271
272
  while ((preMatch = preRefPattern.exec(mapContent)) !== null) {
272
273
  preRefSymbols[`${preMatch[2]}:${preMatch[3]}`] = preMatch[1];
@@ -276,7 +277,10 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
276
277
  const fileCache = {};
277
278
  function readFileCached(filePath) {
278
279
  if (!fileCache[filePath]) {
279
- const fullPath = path.join(cwd, filePath);
280
+ const expandedPath = filePath.startsWith('~/') || filePath.startsWith('~\\')
281
+ ? path.join(homeDir, filePath.slice(2))
282
+ : filePath;
283
+ const fullPath = path.isAbsolute(expandedPath) ? expandedPath : path.join(cwd, expandedPath);
280
284
  if (!fs.existsSync(fullPath)) return null;
281
285
  try {
282
286
  const content = fs.readFileSync(fullPath, 'utf8');
@@ -320,7 +324,15 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
320
324
  // Find where the symbol actually is now
321
325
  const newStart = findSymbolLine(file.content, symbol);
322
326
  if (!newStart) {
323
- unhealable.push({ file: filePath, line: startLine, reason: `Symbol "${symbol}" not found` });
327
+ // A "symbol" that appears nowhere in the file is often descriptive
328
+ // prose from the annotation ("PATCH /api/...", "mission drain"), not
329
+ // a code identifier. Suppress those for in-bounds refs to existing
330
+ // files; still flag out-of-bounds refs and vanished identifiers
331
+ // (camelCase/snake_case words were plausibly real symbols).
332
+ const identifierLike = /[a-z][^\s]*[A-Z]|_/.test(symbol);
333
+ if (outOfBounds || identifierLike) {
334
+ unhealable.push({ file: filePath, line: startLine, reason: `Symbol "${symbol}" not found` });
335
+ }
324
336
  continue;
325
337
  }
326
338
 
@@ -404,28 +416,30 @@ function findFunctionEnd(lines, startLine) {
404
416
  function extractSymbol(context) {
405
417
  if (!context) return null;
406
418
 
407
- // Clean up the context
408
- const cleaned = context.trim()
409
- .replace(/`/g, '') // Strip backticks
410
- .replace(/^[\(\[—\-:]+\s*/, '') // Remove leading punctuation
411
- .replace(/[\)\]]+$/, '') // Remove trailing brackets
419
+ const original = context;
420
+
421
+ // Clean up the context without stripping parentheses from a symbol ending in ().
422
+ const unquoted = context.trim().replace(/`/g, '');
423
+ const wrapper = unquoted.match(/^([\(\[])/)?.[1];
424
+ let cleaned = unquoted
425
+ .replace(/^[\(\[—\-:]+\s*/, '')
412
426
  .trim();
427
+ if (wrapper === '(') cleaned = cleaned.replace(/\)$/, '').trim();
428
+ if (wrapper === '[') cleaned = cleaned.replace(/\]$/, '').trim();
413
429
 
414
430
  if (!cleaned) return null;
415
431
 
416
- // Try to extract a function/class/variable name
417
- // Pattern: word that looks like an identifier
418
- const identifierMatch = cleaned.match(/\b([a-zA-Z_][a-zA-Z0-9_]*(?:Atris|Entry|Handler|Cmd|Function|Class)?)\b/i);
432
+ const tokenMatch = cleaned.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\(\))?/);
433
+ if (!tokenMatch) return null;
419
434
 
420
- if (identifierMatch) {
421
- return identifierMatch[1];
422
- }
435
+ const symbol = tokenMatch[1];
436
+ const token = tokenMatch[0];
437
+ const codeShaped = symbol.includes('_') || /[a-z][A-Z]/.test(symbol) || token.endsWith('()');
438
+ if (codeShaped) return symbol;
423
439
 
424
- // Fallback: first word
425
- const firstWord = cleaned.split(/\s+/)[0];
426
- if (firstWord && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(firstWord)) {
427
- return firstWord;
428
- }
440
+ const isSingleWord = cleaned === symbol;
441
+ const isBackticked = original.includes(`\`${symbol}\``);
442
+ if (isSingleWord && isBackticked) return symbol;
429
443
 
430
444
  return null;
431
445
  }
@@ -445,7 +459,7 @@ function findSymbolLine(fileContent, symbol) {
445
459
  new RegExp(`^\\s*class\\s+${esc}\\b`), // class name
446
460
  new RegExp(`^\\s*${esc}\\s*[:(]`), // name: or name(
447
461
  new RegExp(`exports\\.${esc}\\s*=`), // exports.name =
448
- new RegExp(`^\\s*def\\s+${esc}\\s*\\(`), // def name( (Python)
462
+ new RegExp(`^\\s*(async\\s+)?def\\s+${esc}\\s*\\(`), // [async] def name( (Python)
449
463
  ];
450
464
 
451
465
  for (const pattern of patterns) {
@@ -465,15 +479,27 @@ function findSymbolLine(fileContent, symbol) {
465
479
  */
466
480
  function findStalePages(cwd, atrisDir) {
467
481
  const stalePages = [];
482
+ const skippedDirs = new Set(['archive', 'runs', 'qa', 'node_modules', '.git']);
483
+ const currentYear = new Date().getFullYear();
484
+
485
+ function shouldSkipDir(dir, name) {
486
+ if (skippedDirs.has(name)) return true;
487
+ if (/^\d{4}$/.test(name) && path.basename(path.dirname(dir)) === 'logs') {
488
+ return Number(name) < currentYear;
489
+ }
490
+ return false;
491
+ }
468
492
 
469
493
  function scanDir(dir) {
470
494
  if (!fs.existsSync(dir)) return;
471
495
  const entries = fs.readdirSync(dir, { withFileTypes: true });
472
496
  for (const entry of entries) {
473
497
  const fullPath = path.join(dir, entry.name);
474
- if (entry.isDirectory() && entry.name !== 'archive' && entry.name !== 'node_modules') {
498
+ if (entry.isDirectory()) {
499
+ if (shouldSkipDir(fullPath, entry.name)) continue;
475
500
  scanDir(fullPath);
476
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
501
+ } else if (entry.isFile()) {
502
+ if (!entry.name.endsWith('.md')) continue;
477
503
  const result = checkPageStaleness(cwd, fullPath);
478
504
  if (result) stalePages.push(result);
479
505
  }