atris 3.15.14 → 3.15.22

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 (93) hide show
  1. package/AGENTS.md +84 -8
  2. package/README.md +5 -1
  3. package/atris/AGENTS.md +46 -1
  4. package/atris/CLAUDE.md +36 -1
  5. package/atris/GEMINI.md +14 -1
  6. package/atris/atris.md +12 -1
  7. package/atris/atrisDev.md +3 -2
  8. package/atris/context/README.md +11 -0
  9. package/atris/features/company-brain-sync/validate.md +5 -5
  10. package/atris/learnings.jsonl +1 -0
  11. package/atris/policies/atris-design.md +2 -0
  12. package/atris/skills/aeo/SKILL.md +2 -2
  13. package/atris/skills/atris/SKILL.md +15 -62
  14. package/atris/skills/design/SKILL.md +2 -0
  15. package/atris/skills/imessage/SKILL.md +19 -2
  16. package/atris/skills/loop/SKILL.md +6 -5
  17. package/atris/skills/magic-inbox/SKILL.md +1 -1
  18. package/atris/team/_template/MEMBER.md +23 -1
  19. package/atris/team/brainstormer/START_HERE.md +6 -0
  20. package/atris/team/executor/MEMBER.md +13 -0
  21. package/atris/team/executor/START_HERE.md +6 -0
  22. package/atris/team/launcher/START_HERE.md +6 -0
  23. package/atris/team/mission-lead/MEMBER.md +39 -0
  24. package/atris/team/mission-lead/MISSION.md +33 -0
  25. package/atris/team/mission-lead/START_HERE.md +6 -0
  26. package/atris/team/navigator/MEMBER.md +11 -0
  27. package/atris/team/navigator/START_HERE.md +6 -0
  28. package/atris/team/opus-overnight/MEMBER.md +39 -0
  29. package/atris/team/opus-overnight/MISSION.md +61 -0
  30. package/atris/team/opus-overnight/START_HERE.md +6 -0
  31. package/atris/team/opus-overnight/STEERING.md +35 -0
  32. package/atris/team/researcher/START_HERE.md +6 -0
  33. package/atris/team/validator/MEMBER.md +26 -6
  34. package/atris/team/validator/START_HERE.md +6 -0
  35. package/atris/wiki/concepts/agent-activation-contract.md +79 -0
  36. package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
  37. package/atris/wiki/index.md +27 -0
  38. package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
  39. package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
  40. package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
  41. package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
  42. package/atris.md +49 -13
  43. package/bin/atris.js +660 -22
  44. package/commands/activate.js +12 -3
  45. package/commands/aeo.js +1 -1
  46. package/commands/align.js +10 -10
  47. package/commands/analytics.js +9 -4
  48. package/commands/app.js +2 -0
  49. package/commands/apps.js +276 -0
  50. package/commands/auth.js +1 -1
  51. package/commands/autopilot.js +74 -5
  52. package/commands/brain.js +536 -61
  53. package/commands/brainstorm.js +12 -12
  54. package/commands/business-sync.js +142 -24
  55. package/commands/clean.js +9 -6
  56. package/commands/codex-goal.js +311 -0
  57. package/commands/errors.js +11 -1
  58. package/commands/feedback.js +55 -17
  59. package/commands/fork.js +2 -2
  60. package/commands/gm.js +376 -0
  61. package/commands/init.js +80 -3
  62. package/commands/integrations.js +524 -0
  63. package/commands/learn.js +25 -16
  64. package/commands/lesson.js +41 -0
  65. package/commands/lifecycle.js +2 -2
  66. package/commands/member.js +2416 -9
  67. package/commands/mission.js +1776 -0
  68. package/commands/now.js +48 -7
  69. package/commands/play.js +425 -0
  70. package/commands/publish.js +2 -1
  71. package/commands/pull.js +72 -29
  72. package/commands/push.js +163 -18
  73. package/commands/review.js +51 -13
  74. package/commands/skill.js +2 -2
  75. package/commands/soul.js +19 -13
  76. package/commands/status.js +6 -1
  77. package/commands/sync.js +5 -4
  78. package/commands/task.js +1041 -147
  79. package/commands/terminal.js +5 -5
  80. package/commands/verify.js +7 -5
  81. package/commands/visualize.js +7 -0
  82. package/commands/wiki.js +53 -16
  83. package/commands/workflow.js +298 -54
  84. package/commands/workspace-clean.js +1 -1
  85. package/commands/worktree.js +468 -0
  86. package/commands/xp.js +1608 -0
  87. package/lib/manifest.js +34 -4
  88. package/lib/scorecard.js +3 -2
  89. package/lib/task-db.js +408 -27
  90. package/lib/todo-fallback.js +28 -2
  91. package/lib/todo.js +5 -3
  92. package/package.json +23 -2
  93. package/utils/update-check.js +51 -1
@@ -11,18 +11,7 @@ const pkg = require('../package.json');
11
11
 
12
12
  async function brainstormAtris() {
13
13
  const args = process.argv.slice(3);
14
- const targetDir = path.join(process.cwd(), 'atris');
15
- if (!fs.existsSync(targetDir)) {
16
- throw new Error('atris/ folder not found. Run "atris init" first.');
17
- }
18
-
19
- ensureLogDirectory();
20
- const { logFile, dateFormatted } = getLogPath();
21
- if (!fs.existsSync(logFile)) {
22
- createLogFile(logFile, dateFormatted);
23
- }
24
-
25
- if (args.includes('--help') || args.includes('-h')) {
14
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
26
15
  console.log('');
27
16
  console.log('Usage: atris brainstorm [idea] [--cloud]');
28
17
  console.log('');
@@ -37,6 +26,17 @@ async function brainstormAtris() {
37
26
  return;
38
27
  }
39
28
 
29
+ const targetDir = path.join(process.cwd(), 'atris');
30
+ if (!fs.existsSync(targetDir)) {
31
+ throw new Error('atris/ folder not found. Run "atris init" first.');
32
+ }
33
+
34
+ ensureLogDirectory();
35
+ const { logFile, dateFormatted } = getLogPath();
36
+ if (!fs.existsSync(logFile)) {
37
+ createLogFile(logFile, dateFormatted);
38
+ }
39
+
40
40
  const useCloudJournal = args.includes('--cloud') && !args.includes('--no-cloud');
41
41
  const topicFromArgs = args.filter((arg) => !arg.startsWith('-')).join(' ').trim() || null;
42
42
 
@@ -3,7 +3,11 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { loadManifest } = require('../lib/manifest');
5
5
 
6
- const WATCH_IGNORED_DIRS = new Set(['.git', '.atris', '.claude', 'node_modules', '__pycache__']);
6
+ const WATCH_IGNORED_DIRS = new Set([
7
+ '.git', '.atris', '.claude', '.cursor', '.next', '.cache',
8
+ 'node_modules', '__pycache__', 'venv', '.venv', 'dist', 'build',
9
+ 'coverage', 'tmp', 'temp',
10
+ ]);
7
11
  const WATCH_IGNORED_FILES = new Set(['.DS_Store']);
8
12
 
9
13
  function commandLine(args) {
@@ -33,8 +37,9 @@ function parseBusinessSyncArgs(args = []) {
33
37
  const watch = args.includes('--watch');
34
38
  const intervalSec = Number.parseInt(parseFlagValue(args, '--interval', '60'), 10);
35
39
  const debounceSec = Number.parseInt(parseFlagValue(args, '--debounce', '5'), 10);
40
+ const help = args.includes('--help') || args.includes('-h') || positional[0] === 'help';
36
41
 
37
- return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve };
42
+ return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, help };
38
43
  }
39
44
 
40
45
  function readBusinessSlug(cwd = process.cwd()) {
@@ -75,8 +80,7 @@ function shouldIgnoreWatchPath(relativePath) {
75
80
  return WATCH_IGNORED_FILES.has(path.basename(relativePath));
76
81
  }
77
82
 
78
- function collectBrainSnapshot(root) {
79
- const brainDir = path.join(root, 'atris');
83
+ function collectWorkspaceSnapshot(root) {
80
84
  const snapshot = new Map();
81
85
 
82
86
  function walk(dir) {
@@ -89,7 +93,7 @@ function collectBrainSnapshot(root) {
89
93
 
90
94
  for (const entry of entries) {
91
95
  const full = path.join(dir, entry.name);
92
- const rel = path.relative(brainDir, full);
96
+ const rel = path.relative(root, full);
93
97
  if (shouldIgnoreWatchPath(rel)) continue;
94
98
  if (entry.isDirectory()) {
95
99
  walk(full);
@@ -104,10 +108,12 @@ function collectBrainSnapshot(root) {
104
108
  }
105
109
  }
106
110
 
107
- walk(brainDir);
111
+ walk(root);
108
112
  return snapshot;
109
113
  }
110
114
 
115
+ const collectBrainSnapshot = collectWorkspaceSnapshot;
116
+
111
117
  function snapshotsDiffer(before, after) {
112
118
  if (before.size !== after.size) return true;
113
119
  for (const [key, value] of before.entries()) {
@@ -152,8 +158,7 @@ function writeSyncStatus(cwd, payload = {}) {
152
158
  }, null, 2) + '\n', 'utf8');
153
159
  }
154
160
 
155
- function countBrainFiles(cwd) {
156
- const brainDir = path.join(cwd, 'atris');
161
+ function countWorkspaceFiles(cwd) {
157
162
  let count = 0;
158
163
 
159
164
  function walk(dir) {
@@ -165,17 +170,78 @@ function countBrainFiles(cwd) {
165
170
  }
166
171
  for (const entry of entries) {
167
172
  const full = path.join(dir, entry.name);
168
- const rel = path.relative(brainDir, full);
173
+ const rel = path.relative(cwd, full);
169
174
  if (shouldIgnoreWatchPath(rel)) continue;
170
175
  if (entry.isDirectory()) walk(full);
171
176
  else if (entry.isFile()) count += 1;
172
177
  }
173
178
  }
174
179
 
175
- walk(brainDir);
180
+ walk(cwd);
176
181
  return count;
177
182
  }
178
183
 
184
+ function countFilesUnder(dir) {
185
+ let count = 0;
186
+
187
+ function walk(current) {
188
+ let entries;
189
+ try {
190
+ entries = fs.readdirSync(current, { withFileTypes: true });
191
+ } catch {
192
+ return;
193
+ }
194
+ for (const entry of entries) {
195
+ const full = path.join(current, entry.name);
196
+ if (entry.isDirectory()) walk(full);
197
+ else if (entry.isFile()) count += 1;
198
+ }
199
+ }
200
+
201
+ walk(dir);
202
+ return count;
203
+ }
204
+
205
+ function collectWorkspaceWarnings(cwd, slug) {
206
+ const warnings = [];
207
+ if (slug) {
208
+ const nested = path.join(cwd, slug);
209
+ if (fs.existsSync(nested)) {
210
+ try {
211
+ if (fs.statSync(nested).isDirectory()) {
212
+ warnings.push(`nested workspace folder: ${slug}/ (${countFilesUnder(nested)} files)`);
213
+ }
214
+ } catch {
215
+ // Ignore races while editors or sync tools move folders.
216
+ }
217
+ }
218
+ }
219
+
220
+ const syncArtifacts = [];
221
+ function walk(dir) {
222
+ let entries;
223
+ try {
224
+ entries = fs.readdirSync(dir, { withFileTypes: true });
225
+ } catch {
226
+ return;
227
+ }
228
+ for (const entry of entries) {
229
+ const full = path.join(dir, entry.name);
230
+ const rel = path.relative(cwd, full);
231
+ if (shouldIgnoreWatchPath(rel)) continue;
232
+ if (entry.isDirectory()) walk(full);
233
+ else if (entry.isFile() && ['.remote', '.local', '.base', '.cloud'].some((suffix) => entry.name.endsWith(suffix))) {
234
+ syncArtifacts.push(rel.replace(/\\/g, '/'));
235
+ }
236
+ }
237
+ }
238
+ walk(cwd);
239
+ if (syncArtifacts.length > 0) {
240
+ warnings.push(`sync review artifacts outside .atris/: ${syncArtifacts.length}`);
241
+ }
242
+ return warnings;
243
+ }
244
+
179
245
  function listConflictSummaries(cwd) {
180
246
  const conflictsDir = path.join(cwd, '.atris', 'sync', 'conflicts');
181
247
  const summaries = [];
@@ -206,30 +272,36 @@ function collectLocalSyncStatus(cwd = process.cwd(), options = {}) {
206
272
  const heartbeat = readJsonFile(syncStatusPath(cwd));
207
273
  const conflictSummaries = listConflictSummaries(cwd);
208
274
  const brainDir = path.join(cwd, 'atris');
275
+ const workspaceFileCount = countWorkspaceFiles(cwd);
209
276
  const manifestRootMatches = !manifest || !manifest.workspace_root || sameRealPath(manifest.workspace_root, cwd);
277
+ const warnings = collectWorkspaceWarnings(cwd, slug);
210
278
 
211
279
  return {
212
280
  slug,
213
281
  cwd,
214
282
  brainDir,
215
283
  brainExists: fs.existsSync(brainDir),
216
- brainFileCount: countBrainFiles(cwd),
284
+ brainFileCount: workspaceFileCount,
285
+ workspaceFileCount,
217
286
  conflictCount: conflictSummaries.length,
218
287
  latestConflict: conflictSummaries[conflictSummaries.length - 1] || null,
219
288
  lastSync: manifest && manifest.last_sync ? manifest.last_sync : null,
220
289
  manifestRoot: manifest && manifest.workspace_root ? manifest.workspace_root : null,
221
290
  manifestRootMatches,
222
291
  heartbeat,
292
+ warnings,
223
293
  };
224
294
  }
225
295
 
226
296
  function renderLocalSyncStatus(status) {
227
297
  const lines = [];
228
- const fileLabel = status.brainFileCount === 1 ? 'file' : 'files';
229
- lines.push('Company brain status');
298
+ const fileLabel = status.workspaceFileCount === 1 ? 'file' : 'files';
299
+ lines.push('Business workspace sync status');
230
300
  lines.push(` business: ${status.slug || 'not detected'}`);
231
301
  lines.push(` folder: ${status.cwd}`);
232
- lines.push(` brain: ${status.brainExists ? `atris/ (${status.brainFileCount} ${fileLabel})` : 'missing atris/'}`);
302
+ lines.push(` workspace: ${status.workspaceFileCount} ${fileLabel} (${status.brainExists ? 'atris/ present' : 'missing atris/'})`);
303
+ lines.push(' loop: Pull -> Review -> Publish');
304
+ lines.push(' quest gate: exact files beat broad pushes');
233
305
  lines.push(` last cloud sync: ${status.lastSync || 'never on this machine'}`);
234
306
  if (status.manifestRoot && !status.manifestRootMatches) {
235
307
  lines.push(` manifest: from another folder (${status.manifestRoot})`);
@@ -242,16 +314,43 @@ function renderLocalSyncStatus(status) {
242
314
  } else {
243
315
  lines.push(' conflicts: none');
244
316
  }
317
+ if (status.warnings && status.warnings.length > 0) {
318
+ lines.push(` warnings: ${status.warnings.length}`);
319
+ status.warnings.slice(0, 3).forEach((warning) => lines.push(` - ${warning}`));
320
+ } else {
321
+ lines.push(' warnings: none');
322
+ }
245
323
  if (status.heartbeat && status.heartbeat.updated_at) {
246
324
  lines.push(` watcher: last heartbeat ${status.heartbeat.updated_at} (${status.heartbeat.state || 'unknown'})`);
247
325
  } else {
248
326
  lines.push(' watcher: no heartbeat yet');
249
327
  }
250
328
  lines.push('');
251
- lines.push('Next: run `atris sync --dry-run` to preview, or `atris sync --watch` to keep this brain live.');
329
+ lines.push('Next: run `atris sync --dry-run` to preview the quest, or `atris sync --review` if conflicts exist.');
252
330
  return `${lines.join('\n')}\n`;
253
331
  }
254
332
 
333
+ function renderBusinessSyncHelp() {
334
+ return [
335
+ 'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--review] [--resolve local|cloud|both|merge] [--timeout 120]',
336
+ '',
337
+ 'Safe loop:',
338
+ ' Pull -> Review -> Publish',
339
+ '',
340
+ 'Commands:',
341
+ ' atris sync --status Show local sync health',
342
+ ' atris sync --dry-run Preview pull and publish plans without writing cloud',
343
+ ' atris sync --review Read the latest conflict packet',
344
+ ' atris sync --resolve cloud|local|merge',
345
+ ' atris sync --watch Keep the workspace live with the same safety gates',
346
+ '',
347
+ 'Publish safety:',
348
+ ' Large unscoped pushes, nested workspace folders, and *.remote artifacts are blocked.',
349
+ ' Use exact --only paths for repairs, or explicit push override flags after review.',
350
+ '',
351
+ ].join('\n');
352
+ }
353
+
255
354
  function renderLatestConflictReview(cwd = process.cwd()) {
256
355
  const summaries = listConflictSummaries(cwd);
257
356
  if (summaries.length === 0) {
@@ -297,7 +396,6 @@ function collectConflictResolutionEntries(cwd = process.cwd()) {
297
396
  const localPath = full;
298
397
  const remotePath = full.replace(/\.local$/, '.remote');
299
398
  const targetRel = path.relative(dir, full).replace(/\\/g, '/').replace(/\.local$/, '');
300
- if (!targetRel.startsWith('atris/')) continue;
301
399
  entries.push({
302
400
  targetRel,
303
401
  basePath: full.replace(/\.local$/, '.base'),
@@ -321,6 +419,13 @@ function assertWorkspaceTarget(cwd, targetRel) {
321
419
  return targetPath;
322
420
  }
323
421
 
422
+ function cleanupResolvedConflictSidecars(cwd, targetRel, { keepCloud = false } = {}) {
423
+ const suffixes = ['.base', '.local', '.remote', ...(keepCloud ? [] : ['.cloud'])];
424
+ for (const suffix of suffixes) {
425
+ fs.rmSync(assertWorkspaceTarget(cwd, `${targetRel}${suffix}`), { force: true });
426
+ }
427
+ }
428
+
324
429
  function changedRange(baseLines, changedLines) {
325
430
  let start = 0;
326
431
  while (start < baseLines.length && start < changedLines.length && baseLines[start] === changedLines[start]) {
@@ -485,6 +590,7 @@ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
485
590
  fs.mkdirSync(path.dirname(remoteCopyPath), { recursive: true });
486
591
  fs.copyFileSync(entry.remotePath, remoteCopyPath);
487
592
  }
593
+ cleanupResolvedConflictSidecars(cwd, entry.targetRel, { keepCloud: true });
488
594
  resolved.push(entry.targetRel);
489
595
  continue;
490
596
  }
@@ -505,6 +611,7 @@ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
505
611
  }
506
612
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
507
613
  fs.writeFileSync(targetPath, merged.content, 'utf8');
614
+ cleanupResolvedConflictSidecars(cwd, entry.targetRel);
508
615
  resolved.push(entry.targetRel);
509
616
  continue;
510
617
  }
@@ -513,6 +620,7 @@ function resolveLatestConflict(cwd = process.cwd(), strategy = 'local') {
513
620
  if (!fs.existsSync(sourcePath)) continue;
514
621
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
515
622
  fs.copyFileSync(sourcePath, targetPath);
623
+ cleanupResolvedConflictSidecars(cwd, entry.targetRel);
516
624
  resolved.push(entry.targetRel);
517
625
  }
518
626
 
@@ -593,6 +701,11 @@ async function runSyncCycle(plan, cwd, options = {}) {
593
701
  async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
594
702
  const options = resolveBusinessSyncOptions(args, cwd);
595
703
 
704
+ if (options.help) {
705
+ process.stdout.write(renderBusinessSyncHelp());
706
+ return;
707
+ }
708
+
596
709
  if (options.status) {
597
710
  process.stdout.write(renderLocalSyncStatus(collectLocalSyncStatus(cwd, options)));
598
711
  return;
@@ -611,14 +724,16 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
611
724
  const plan = buildBusinessSyncPlan(options);
612
725
 
613
726
  if (!plan) {
614
- console.error('Usage: atris sync [business] [--dry-run] [--watch] [--status] [--review] [--resolve local|cloud|both|merge] [--timeout 120]');
727
+ console.error(renderBusinessSyncHelp().trimEnd());
615
728
  console.error('Run inside a business workspace or pass a business slug.');
616
729
  process.exit(1);
617
730
  }
618
731
 
619
732
  console.log('');
620
- console.log(`Syncing ${options.slug} knowledge wiki...`);
621
- console.log(' scope: atris/');
733
+ console.log(`Syncing ${options.slug} business workspace...`);
734
+ console.log(' scope: full workspace');
735
+ console.log(' loop: Pull -> Review -> Publish');
736
+ console.log(' quest gate: exact files beat broad pushes');
622
737
  if (options.watch) {
623
738
  console.log(` watch: on (${options.intervalSec}s interval, ${options.debounceSec}s debounce)`);
624
739
  }
@@ -632,18 +747,18 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
632
747
  });
633
748
  if (!options.watch) return;
634
749
 
635
- let lastSnapshot = collectBrainSnapshot(cwd);
750
+ let lastSnapshot = collectWorkspaceSnapshot(cwd);
636
751
  let running = false;
637
752
  let quietTicks = 0;
638
753
  let pendingLocal = false;
639
754
 
640
755
  console.log('');
641
- console.log('Company brain sync is watching atris/. Press Ctrl+C to stop.');
756
+ console.log('Business workspace sync is watching this folder. Press Ctrl+C to stop.');
642
757
 
643
758
  const tickMs = 1000;
644
759
  setInterval(async () => {
645
760
  if (running) return;
646
- const current = collectBrainSnapshot(cwd);
761
+ const current = collectWorkspaceSnapshot(cwd);
647
762
  if (snapshotsDiffer(lastSnapshot, current)) {
648
763
  pendingLocal = true;
649
764
  quietTicks = 0;
@@ -664,14 +779,14 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
664
779
  running = true;
665
780
  try {
666
781
  console.log('');
667
- console.log(shouldLocalSync ? 'Local brain changed. Syncing...' : 'Checking cloud brain...');
782
+ console.log(shouldLocalSync ? 'Local workspace changed. Syncing...' : 'Checking cloud workspace...');
668
783
  await runSyncCycle(plan, cwd, {
669
784
  dryRun: options.dryRun,
670
785
  slug: options.slug,
671
786
  writeStatus: !options.dryRun,
672
787
  watch: true,
673
788
  });
674
- lastSnapshot = collectBrainSnapshot(cwd);
789
+ lastSnapshot = collectWorkspaceSnapshot(cwd);
675
790
  pendingLocal = false;
676
791
  quietTicks = 0;
677
792
  } catch (err) {
@@ -699,6 +814,8 @@ module.exports = {
699
814
  buildBusinessSyncPlan,
700
815
  canPreviewPush,
701
816
  collectBrainSnapshot,
817
+ collectWorkspaceSnapshot,
818
+ collectWorkspaceWarnings,
702
819
  collectLocalSyncStatus,
703
820
  collectConflictResolutionEntries,
704
821
  describeWatchFailure,
@@ -706,6 +823,7 @@ module.exports = {
706
823
  readBusinessSlug,
707
824
  renderLatestConflictReview,
708
825
  renderLocalSyncStatus,
826
+ renderBusinessSyncHelp,
709
827
  resolveLatestConflict,
710
828
  resolveBusinessSyncOptions,
711
829
  safeLineMerge,
package/commands/clean.js CHANGED
@@ -60,7 +60,7 @@ function cleanAtris(options = {}) {
60
60
 
61
61
  // Stale tasks
62
62
  if (staleTasks.length > 0) {
63
- console.log(`⚠ ${staleTasks.length} stale task(s) (claimed >3 days, not completed):`);
63
+ console.log(`⚠ ${staleTasks.length} stale ${staleTasks.length === 1 ? 'task' : 'tasks'} (claimed >3 days, not completed):`);
64
64
  staleTasks.forEach(task => {
65
65
  console.log(` • ${task.title.substring(0, 50)}${task.title.length > 50 ? '...' : ''}`);
66
66
  });
@@ -71,12 +71,13 @@ function cleanAtris(options = {}) {
71
71
 
72
72
  // Healed refs
73
73
  if (healed > 0) {
74
- console.log(`✓ Healed ${healed} MAP.md reference(s)`);
74
+ const verb = options.dryRun ? 'Would heal' : 'Healed';
75
+ console.log(`✓ ${verb} ${healed} MAP.md ${healed === 1 ? 'reference' : 'references'}`);
75
76
  }
76
77
 
77
78
  // Unhealable refs
78
79
  if (unhealable.length > 0) {
79
- console.log(`⚠ ${unhealable.length} MAP.md ref(s) couldn't be healed:`);
80
+ console.log(`⚠ ${unhealable.length} MAP.md ${unhealable.length === 1 ? 'ref' : 'refs'} couldn't be healed:`);
80
81
  unhealable.slice(0, 3).forEach(ref => {
81
82
  console.log(` • ${ref.file}:${ref.line} — ${ref.reason}`);
82
83
  });
@@ -90,14 +91,16 @@ function cleanAtris(options = {}) {
90
91
 
91
92
  // Archived journals
92
93
  if (archived > 0) {
93
- console.log(`✓ Archived ${archived} old journal(s)`);
94
+ const verb = options.dryRun ? 'Would archive' : 'Archived';
95
+ console.log(`✓ ${verb} ${archived} old journal(s)`);
94
96
  } else {
95
97
  console.log('✓ No journals need archiving');
96
98
  }
97
99
 
98
100
  // Cleaned sections
99
101
  if (cleaned > 0) {
100
- console.log(`✓ Cleaned ${cleaned} empty section(s)`);
102
+ const verb = options.dryRun ? 'Would clean' : 'Cleaned';
103
+ console.log(`✓ ${verb} ${cleaned} empty section(s)`);
101
104
  }
102
105
 
103
106
  console.log('');
@@ -105,7 +108,7 @@ function cleanAtris(options = {}) {
105
108
 
106
109
  // Stale pages
107
110
  if (stalePages.length > 0) {
108
- console.log(`⚠ ${stalePages.length} stale page(s) (source changed since last compiled):`);
111
+ console.log(`⚠ ${stalePages.length} stale ${stalePages.length === 1 ? 'page' : 'pages'} (source changed since last compiled):`);
109
112
  stalePages.forEach(sp => {
110
113
  console.log(` • ${path.relative(cwd, sp.page)} — stale source: ${sp.staleSource}`);
111
114
  });