atris 3.15.13 → 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 +199 -17
  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
@@ -418,6 +418,520 @@ function imessageRecent(handle, options = {}) {
418
418
  console.log(result.stdout.trim() || 'No recent messages found.');
419
419
  }
420
420
 
421
+ function escapeSqlString(value) {
422
+ return String(value || '').replace(/'/g, "''");
423
+ }
424
+
425
+ function normalizeLookupKey(value) {
426
+ return String(value || '')
427
+ .toLowerCase()
428
+ .replace(/[^a-z0-9@+]+/g, ' ')
429
+ .replace(/\s+/g, ' ')
430
+ .trim();
431
+ }
432
+
433
+ function normalizeImessageHandle(value) {
434
+ const raw = String(value || '').trim();
435
+ if (!raw) return '';
436
+ if (raw.includes('@')) return raw;
437
+ const digits = raw.replace(/[^\d]/g, '');
438
+ if (digits.length === 10) return `+1${digits}`;
439
+ if (digits.length === 11 && digits.startsWith('1')) return `+${digits}`;
440
+ return raw;
441
+ }
442
+
443
+ function normalizeContactLabel(value) {
444
+ return String(value || '')
445
+ .replace(/^_\$!<|>!\$_$/g, '')
446
+ .replace(/^\$!<|>!\$$/g, '')
447
+ .trim() || 'other';
448
+ }
449
+
450
+ function imessageLookupCachePath() {
451
+ return path.join(os.homedir(), '.atris', 'cache', 'imessage-contacts.json');
452
+ }
453
+
454
+ function readImessageLookupCache() {
455
+ const cachePath = imessageLookupCachePath();
456
+ try {
457
+ return JSON.parse(fs.readFileSync(cachePath, 'utf8'));
458
+ } catch {
459
+ return { version: 1, entries: {} };
460
+ }
461
+ }
462
+
463
+ function writeImessageLookupCache(cache) {
464
+ const cachePath = imessageLookupCachePath();
465
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
466
+ fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf8');
467
+ return cachePath;
468
+ }
469
+
470
+ function sleepMs(ms) {
471
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
472
+ }
473
+
474
+ function readLatestOutgoingImessage(handle, sinceMs) {
475
+ const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
476
+ const sinceUnix = Math.max(0, Math.floor(Number(sinceMs || Date.now()) / 1000) - 5);
477
+ const sql = `
478
+ SELECT m.rowid,
479
+ datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
480
+ m.is_sent,
481
+ m.is_delivered,
482
+ m.is_finished,
483
+ COALESCE(m.error, 0) AS error,
484
+ length(COALESCE(m.text,'')) AS text_len
485
+ FROM message m
486
+ JOIN handle h ON h.rowid = m.handle_id
487
+ WHERE h.id = '${escapeSqlString(handle)}'
488
+ AND m.is_from_me = 1
489
+ AND (m.date/1000000000 + 978307200) >= ${sinceUnix}
490
+ ORDER BY m.date DESC
491
+ LIMIT 1;
492
+ `;
493
+ const result = spawnSync('sqlite3', ['-readonly', chatDb, sql], { encoding: 'utf8' });
494
+ if (result.status !== 0) {
495
+ return {
496
+ matched: false,
497
+ error: (result.stderr || 'Failed to verify latest outgoing iMessage.').trim(),
498
+ };
499
+ }
500
+ const row = String(result.stdout || '').trim();
501
+ if (!row) {
502
+ return {
503
+ matched: false,
504
+ error: 'No outgoing Messages row found after send.',
505
+ };
506
+ }
507
+ const [rowid, ts, isSent, isDelivered, isFinished, messageError, textLen] = row.split('|');
508
+ return {
509
+ matched: true,
510
+ rowid,
511
+ timestamp: ts,
512
+ is_sent: Number(isSent) === 1,
513
+ is_delivered: Number(isDelivered) === 1,
514
+ is_finished: Number(isFinished) === 1,
515
+ message_error: Number(messageError) || 0,
516
+ text_readable: Number(textLen) > 0,
517
+ };
518
+ }
519
+
520
+ function imessageVerifyLatestOutgoing(handle, sinceMs, options = {}) {
521
+ const timeoutMs = Math.max(0, Number(options.timeoutMs || 2000));
522
+ const intervalMs = Math.max(25, Number(options.intervalMs || 150));
523
+ const startedAt = Date.now();
524
+ let latest = readLatestOutgoingImessage(handle, sinceMs);
525
+ while (
526
+ latest.matched
527
+ && latest.message_error === 0
528
+ && !(latest.is_sent || latest.is_delivered || latest.is_finished)
529
+ && Date.now() - startedAt < timeoutMs
530
+ ) {
531
+ sleepMs(intervalMs);
532
+ latest = readLatestOutgoingImessage(handle, sinceMs);
533
+ }
534
+ return {
535
+ ...latest,
536
+ settled: Boolean(latest.matched && latest.message_error === 0 && (latest.is_sent || latest.is_delivered || latest.is_finished)),
537
+ waited_ms: Date.now() - startedAt,
538
+ };
539
+ }
540
+
541
+ function parseImessageLookupArgs(args) {
542
+ const options = {
543
+ json: false,
544
+ refresh: false,
545
+ name: '',
546
+ maxAgeMs: 7 * 24 * 60 * 60 * 1000,
547
+ };
548
+ const positional = [];
549
+ for (let i = 0; i < args.length; i += 1) {
550
+ const arg = args[i];
551
+ if (arg === '--json') {
552
+ options.json = true;
553
+ } else if (arg === '--refresh') {
554
+ options.refresh = true;
555
+ } else if (arg === '--name' || arg === '--query') {
556
+ options.name = args[i + 1] || '';
557
+ i += 1;
558
+ } else if (arg === '--max-age-minutes') {
559
+ options.maxAgeMs = Math.max(0, Number(args[i + 1] || 0) * 60 * 1000);
560
+ i += 1;
561
+ } else {
562
+ positional.push(arg);
563
+ }
564
+ }
565
+ if (!options.name && positional.length) options.name = positional.join(' ');
566
+ options.name = String(options.name || '').trim();
567
+ return options;
568
+ }
569
+
570
+ const CONTACT_LOOKUP_SCRIPT = `
571
+ function run(argv) {
572
+ const query = String(argv[0] || '').trim();
573
+ const Contacts = Application('Contacts');
574
+ const selfAlias = /^(me|myself|self|my number|my phone|my contact)$/i.test(query);
575
+ function safe(fn) {
576
+ try {
577
+ const value = fn();
578
+ return value === null || value === undefined ? '' : String(value);
579
+ } catch (error) {
580
+ return '';
581
+ }
582
+ }
583
+ function propertyRows(rows) {
584
+ const out = [];
585
+ try {
586
+ const list = rows();
587
+ for (let i = 0; i < list.length; i += 1) {
588
+ const item = list[i];
589
+ out.push({
590
+ label: safe(function () { return item.label(); }),
591
+ value: safe(function () { return item.value(); })
592
+ });
593
+ }
594
+ } catch (error) {}
595
+ return out.filter(function (row) { return row.value; });
596
+ }
597
+ function contactRow(person) {
598
+ return {
599
+ id: safe(function () { return person.id(); }),
600
+ name: safe(function () { return person.name(); }),
601
+ first_name: safe(function () { return person.firstName(); }),
602
+ last_name: safe(function () { return person.lastName(); }),
603
+ organization: safe(function () { return person.organization(); }),
604
+ phones: propertyRows(function () { return person.phones(); }),
605
+ emails: propertyRows(function () { return person.emails(); })
606
+ };
607
+ }
608
+ let people = [];
609
+ if (selfAlias) {
610
+ if (Contacts.myCard.exists()) people = [Contacts.myCard];
611
+ } else {
612
+ people = Contacts.people.whose({ name: { _contains: query } })();
613
+ if (!people.length && query.indexOf(' ') > -1) {
614
+ people = Contacts.people.whose({ name: { _contains: query.split(/\\s+/)[0] } })();
615
+ }
616
+ }
617
+ const rows = [];
618
+ const limit = Math.min(20, people.length);
619
+ for (let i = 0; i < limit; i += 1) rows.push(contactRow(people[i]));
620
+ return JSON.stringify({ ok: true, query: query, self_alias: selfAlias, contacts_count: people.length, matches: rows });
621
+ }
622
+ `;
623
+
624
+ function scoreImessageContact(query, match, selfAlias = false) {
625
+ if (selfAlias) return 100;
626
+ const q = normalizeLookupKey(query);
627
+ const name = normalizeLookupKey(match.name);
628
+ if (!q || !name) return 0;
629
+ if (name === q) return 95;
630
+ if (name.includes(q)) return 80;
631
+ const tokens = q.split(' ').filter(Boolean);
632
+ const nameTokens = new Set(name.split(' ').filter(Boolean));
633
+ if (tokens.length && tokens.every((token) => nameTokens.has(token))) return 75;
634
+ if (tokens.length === 1 && nameTokens.has(tokens[0])) return 65;
635
+ return 40;
636
+ }
637
+
638
+ function shapeImessageLookupPayload(raw, options, cached = false, cachedAt = null) {
639
+ const query = options.name;
640
+ const selfAlias = Boolean(raw.self_alias);
641
+ const matches = (raw.matches || [])
642
+ .map((match) => {
643
+ const phones = (match.phones || [])
644
+ .map((phone) => ({
645
+ label: normalizeContactLabel(phone.label),
646
+ value: String(phone.value || '').trim(),
647
+ handle: normalizeImessageHandle(phone.value),
648
+ }))
649
+ .filter((phone) => phone.handle);
650
+ const emails = (match.emails || [])
651
+ .map((email) => ({
652
+ label: normalizeContactLabel(email.label),
653
+ value: String(email.value || '').trim(),
654
+ handle: String(email.value || '').trim(),
655
+ }))
656
+ .filter((email) => email.handle);
657
+ const handles = [
658
+ ...phones.map((phone) => ({ type: 'phone', label: phone.label, handle: phone.handle })),
659
+ ...emails.map((email) => ({ type: 'email', label: email.label, handle: email.handle })),
660
+ ];
661
+ return {
662
+ id: match.id || '',
663
+ name: match.name || [match.first_name, match.last_name].filter(Boolean).join(' '),
664
+ phones,
665
+ emails,
666
+ handles,
667
+ primary_handle: handles[0]?.handle || '',
668
+ score: scoreImessageContact(query, match, selfAlias),
669
+ };
670
+ })
671
+ .filter((match) => match.primary_handle)
672
+ .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
673
+ const exactMatches = matches.filter((match) => normalizeLookupKey(match.name) === normalizeLookupKey(query));
674
+ const primary = matches.length === 1
675
+ ? matches[0]
676
+ : exactMatches.length === 1
677
+ ? exactMatches[0]
678
+ : null;
679
+ return {
680
+ ok: true,
681
+ action: 'imessage_lookup',
682
+ provider: 'local_contacts',
683
+ query,
684
+ cached,
685
+ cached_at: cachedAt,
686
+ cache_path: imessageLookupCachePath(),
687
+ match_count: matches.length,
688
+ unique: Boolean(primary),
689
+ ambiguous: matches.length > 1 && !primary,
690
+ primary: primary ? {
691
+ name: primary.name,
692
+ handle: primary.primary_handle,
693
+ handles: primary.handles,
694
+ } : null,
695
+ matches,
696
+ };
697
+ }
698
+
699
+ function printImessageLookupPayload(payload, json = false) {
700
+ if (json) {
701
+ console.log(JSON.stringify(payload, null, 2));
702
+ return;
703
+ }
704
+ if (!payload.ok) {
705
+ console.error(payload.error || 'Contact lookup failed.');
706
+ return;
707
+ }
708
+ if (!payload.matches.length) {
709
+ console.log(`No iMessage contacts found for "${payload.query}".`);
710
+ return;
711
+ }
712
+ if (payload.primary) {
713
+ console.log(`${payload.primary.name}: ${payload.primary.handle}${payload.cached ? ' (cached)' : ''}`);
714
+ return;
715
+ }
716
+ console.log(`Multiple matches for "${payload.query}":`);
717
+ for (const match of payload.matches.slice(0, 8)) {
718
+ console.log(` - ${match.name}: ${match.primary_handle}`);
719
+ }
720
+ }
721
+
722
+ function imessageLookup(args = []) {
723
+ const options = parseImessageLookupArgs(args);
724
+ if (!options.name) {
725
+ printImessageLookupPayload({
726
+ ok: false,
727
+ action: 'imessage_lookup',
728
+ error: 'Usage: atris imessage lookup --name <contact-name> [--json] [--refresh]',
729
+ }, options.json);
730
+ process.exit(1);
731
+ }
732
+
733
+ const key = normalizeLookupKey(options.name);
734
+ const cache = readImessageLookupCache();
735
+ const entry = cache.entries?.[key];
736
+ if (!options.refresh && entry && Date.now() - Number(entry.cached_at || 0) <= options.maxAgeMs) {
737
+ printImessageLookupPayload(shapeImessageLookupPayload(entry.raw, options, true, entry.cached_at), options.json);
738
+ return;
739
+ }
740
+
741
+ const result = spawnSync('osascript', ['-l', 'JavaScript', '-e', CONTACT_LOOKUP_SCRIPT, options.name], {
742
+ encoding: 'utf8',
743
+ timeout: 6000,
744
+ });
745
+ if (result.status !== 0) {
746
+ printImessageLookupPayload({
747
+ ok: false,
748
+ action: 'imessage_lookup',
749
+ query: options.name,
750
+ error: (result.stderr || 'Contacts lookup failed. Grant Contacts automation permission to this terminal or Atris.').trim(),
751
+ }, options.json);
752
+ process.exit(1);
753
+ }
754
+
755
+ let raw;
756
+ try {
757
+ raw = JSON.parse(String(result.stdout || '{}'));
758
+ } catch {
759
+ printImessageLookupPayload({
760
+ ok: false,
761
+ action: 'imessage_lookup',
762
+ query: options.name,
763
+ error: 'Contacts lookup returned invalid JSON.',
764
+ }, options.json);
765
+ process.exit(1);
766
+ }
767
+
768
+ const cachedAt = Date.now();
769
+ const latestCache = readImessageLookupCache();
770
+ latestCache.version = 1;
771
+ latestCache.entries = latestCache.entries || {};
772
+ latestCache.entries[key] = { cached_at: cachedAt, raw };
773
+ writeImessageLookupCache(latestCache);
774
+ printImessageLookupPayload(shapeImessageLookupPayload(raw, options, false, cachedAt), options.json);
775
+ }
776
+
777
+ function parseImessageSendArgs(args) {
778
+ const options = {
779
+ approved: false,
780
+ json: false,
781
+ receipt: false,
782
+ to: '',
783
+ text: '',
784
+ };
785
+ const positional = [];
786
+ for (let i = 0; i < args.length; i += 1) {
787
+ const arg = args[i];
788
+ if (arg === '--approved' || arg === '--confirm-approved') {
789
+ options.approved = true;
790
+ } else if (arg === '--json') {
791
+ options.json = true;
792
+ } else if (arg === '--receipt') {
793
+ options.receipt = true;
794
+ } else if (arg === '--to' || arg === '--handle') {
795
+ options.to = args[i + 1] || '';
796
+ i += 1;
797
+ } else if (arg === '--text' || arg === '--message') {
798
+ options.text = args[i + 1] || '';
799
+ i += 1;
800
+ } else {
801
+ positional.push(arg);
802
+ }
803
+ }
804
+ if (!options.to && positional.length) options.to = positional.shift() || '';
805
+ if (!options.text && positional.length) options.text = positional.join(' ');
806
+ options.to = normalizeImessageHandle(options.to);
807
+ options.text = String(options.text || '').trim();
808
+ return options;
809
+ }
810
+
811
+ function printImessageSendPayload(payload, json = false) {
812
+ if (json) {
813
+ console.log(JSON.stringify(payload, null, 2));
814
+ return;
815
+ }
816
+ if (payload.ok) {
817
+ console.log(`Sent iMessage to ${payload.to}.`);
818
+ if (payload.receipt_path) console.log(`Receipt: ${payload.receipt_path}`);
819
+ } else {
820
+ console.error(payload.error || 'Failed to send iMessage.');
821
+ }
822
+ }
823
+
824
+ function writeImessageSendReceipt(payload) {
825
+ const atrisDir = path.join(process.cwd(), 'atris');
826
+ if (!fs.existsSync(atrisDir)) return '';
827
+ const runsDir = path.join(atrisDir, 'runs');
828
+ fs.mkdirSync(runsDir, { recursive: true });
829
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
830
+ const receiptPath = path.join(runsDir, `imessage-send-${stamp}.md`);
831
+ const lines = [
832
+ '# iMessage Send Receipt',
833
+ '',
834
+ `- Sent at: ${payload.sent_at}`,
835
+ `- Provider: ${payload.provider}`,
836
+ `- To: ${payload.to}`,
837
+ `- Text: ${payload.text}`,
838
+ `- Doctor connected: ${payload.doctor?.connected === true}`,
839
+ `- Send exit: ${payload.osascript?.status}`,
840
+ `- DB verified: ${payload.db_verification?.matched === true}`,
841
+ `- DB settled: ${payload.db_verification?.settled === true}`,
842
+ ];
843
+ fs.writeFileSync(receiptPath, `${lines.join('\n')}\n`, 'utf8');
844
+ return receiptPath;
845
+ }
846
+
847
+ function imessageSend(args = []) {
848
+ const options = parseImessageSendArgs(args);
849
+ const basePayload = {
850
+ ok: false,
851
+ action: 'imessage_send',
852
+ provider: 'local_imessage',
853
+ to: options.to,
854
+ text: options.text,
855
+ approved: options.approved,
856
+ };
857
+
858
+ if (!options.to || !options.text) {
859
+ printImessageSendPayload({
860
+ ...basePayload,
861
+ error: 'Usage: atris imessage send --to <phone-or-email> --text <message> --approved [--json] [--receipt]',
862
+ }, options.json);
863
+ process.exit(1);
864
+ }
865
+
866
+ if (!options.approved) {
867
+ printImessageSendPayload({
868
+ ...basePayload,
869
+ error: 'Refusing to send without --approved after the exact recipient and exact text are confirmed.',
870
+ }, options.json);
871
+ process.exit(1);
872
+ }
873
+
874
+ const doctor = imessageDoctor();
875
+ if (!doctor.connected) {
876
+ printImessageSendPayload({
877
+ ...basePayload,
878
+ doctor,
879
+ error: 'iMessage is not available on this Mac.',
880
+ }, options.json);
881
+ process.exit(1);
882
+ }
883
+
884
+ const sendStartedAt = Date.now();
885
+ const result = spawnSync('osascript', [
886
+ '-e', 'on run argv',
887
+ '-e', 'set targetHandle to item 1 of argv',
888
+ '-e', 'set messageText to item 2 of argv',
889
+ '-e', 'tell application "Messages"',
890
+ '-e', 'set targetService to 1st service whose service type = iMessage',
891
+ '-e', 'set targetBuddy to buddy targetHandle of targetService',
892
+ '-e', 'send messageText to targetBuddy',
893
+ '-e', 'end tell',
894
+ '-e', 'return targetHandle',
895
+ '-e', 'end run',
896
+ options.to,
897
+ options.text,
898
+ ], {
899
+ encoding: 'utf8',
900
+ timeout: 10000,
901
+ });
902
+
903
+ const payload = {
904
+ ...basePayload,
905
+ ok: result.status === 0,
906
+ sent_at: new Date().toISOString(),
907
+ doctor: {
908
+ connected: doctor.connected,
909
+ checks: doctor.checks,
910
+ },
911
+ osascript: {
912
+ status: result.status,
913
+ signal: result.signal || null,
914
+ stdout: String(result.stdout || '').trim(),
915
+ stderr: String(result.stderr || '').trim(),
916
+ },
917
+ };
918
+
919
+ if (payload.ok) {
920
+ payload.db_verification = imessageVerifyLatestOutgoing(options.to, sendStartedAt);
921
+ }
922
+
923
+ if (payload.ok && options.receipt) {
924
+ payload.receipt_path = writeImessageSendReceipt(payload);
925
+ }
926
+
927
+ if (!payload.ok) {
928
+ payload.error = payload.osascript.stderr || 'Messages AppleScript send failed.';
929
+ }
930
+
931
+ printImessageSendPayload(payload, options.json);
932
+ if (!payload.ok) process.exit(1);
933
+ }
934
+
421
935
  async function imessageCommand(subcommand, ...args) {
422
936
  switch (subcommand) {
423
937
  case 'doctor': {
@@ -434,10 +948,20 @@ async function imessageCommand(subcommand, ...args) {
434
948
  imessageRecent(handle, { limit, json: args.includes('--json') });
435
949
  break;
436
950
  }
951
+ case 'lookup': {
952
+ imessageLookup(args);
953
+ break;
954
+ }
955
+ case 'send': {
956
+ imessageSend(args);
957
+ break;
958
+ }
437
959
  default:
438
960
  console.log('iMessage commands:');
439
961
  console.log(' atris imessage doctor [--json] - Check local Messages access');
962
+ console.log(' atris imessage lookup --name <name> [--json] [--refresh]');
440
963
  console.log(' atris imessage recent <handle> - Read recent local messages');
964
+ console.log(' atris imessage send --to <handle> --text <text> --approved [--json] [--receipt]');
441
965
  }
442
966
  }
443
967
 
package/commands/learn.js CHANGED
@@ -68,7 +68,7 @@ function showSearch(query) {
68
68
  }
69
69
 
70
70
  console.log('');
71
- console.log(` Search: "${query}" — ${results.length} result(s)`);
71
+ console.log(` Search: "${query}" — ${results.length} ${results.length === 1 ? 'result' : 'results'}`);
72
72
  console.log('');
73
73
  for (const e of results) {
74
74
  const conf = e._effectiveConfidence;
@@ -316,7 +316,7 @@ function harvestFromJournals() {
316
316
  }
317
317
 
318
318
  console.log('');
319
- console.log(` Found ${fresh.length} new note(s) to harvest:`);
319
+ console.log(` Found ${fresh.length} new ${fresh.length === 1 ? 'note' : 'notes'} to harvest:`);
320
320
  console.log('');
321
321
  for (let i = 0; i < fresh.length; i++) {
322
322
  const c = fresh[i];
@@ -347,7 +347,29 @@ function getLearningCount() {
347
347
  /**
348
348
  * Main entry point for `atris learn [subcommand] [args]`
349
349
  */
350
+ function showLearnHelp() {
351
+ console.log('');
352
+ console.log(' Usage: atris learn [command]');
353
+ console.log('');
354
+ console.log(' Commands:');
355
+ console.log(' (none) Show recent learnings');
356
+ console.log(' add Add a learning interactively');
357
+ console.log(' log <json> Add programmatically (for agents)');
358
+ console.log(' search <q> Search learnings by keyword');
359
+ console.log(' harvest Extract learnings from journal Notes');
360
+ console.log(' prune Check for stale/contradictory entries');
361
+ console.log(' stats Show learning statistics');
362
+ console.log(' export Export as markdown');
363
+ console.log(' count Print learning count (for integrations)');
364
+ console.log('');
365
+ }
366
+
350
367
  function learnAtris(subcommand, ...args) {
368
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h' || args.includes('--help') || args.includes('-h')) {
369
+ showLearnHelp();
370
+ return;
371
+ }
372
+
351
373
  const atrisDir = path.join(process.cwd(), 'atris');
352
374
  if (!fs.existsSync(atrisDir)) {
353
375
  console.error(' ✗ atris/ folder not found. Run "atris init" first.');
@@ -384,20 +406,7 @@ function learnAtris(subcommand, ...args) {
384
406
  harvestFromJournals();
385
407
  break;
386
408
  default:
387
- console.log('');
388
- console.log(' Usage: atris learn [command]');
389
- console.log('');
390
- console.log(' Commands:');
391
- console.log(' (none) Show recent learnings');
392
- console.log(' add Add a learning interactively');
393
- console.log(' log <json> Add programmatically (for agents)');
394
- console.log(' search <q> Search learnings by keyword');
395
- console.log(' harvest Extract learnings from journal Notes');
396
- console.log(' prune Check for stale/contradictory entries');
397
- console.log(' stats Show learning statistics');
398
- console.log(' export Export as markdown');
399
- console.log(' count Print learning count (for integrations)');
400
- console.log('');
409
+ showLearnHelp();
401
410
  break;
402
411
  }
403
412
  }
@@ -0,0 +1,41 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { writeLesson } = require('./autopilot');
4
+
5
+ function lessonAtris(subcommand, ...args) {
6
+ const atrisDir = path.join(process.cwd(), 'atris');
7
+ if (!fs.existsSync(atrisDir)) {
8
+ console.error(' ✗ atris/ folder not found. Run "atris init" first.');
9
+ process.exit(1);
10
+ }
11
+
12
+ if (subcommand !== 'add') {
13
+ console.log('');
14
+ console.log(' Usage: atris lesson add <slug> <pass|fail> "<text>"');
15
+ console.log('');
16
+ process.exit(subcommand ? 1 : 0);
17
+ }
18
+
19
+ const [slug, status, ...messageParts] = args;
20
+ const explanation = messageParts.join(' ').trim();
21
+
22
+ if (!slug || !/^[a-z0-9-]+$/.test(slug)) {
23
+ console.error(' ✗ slug must be kebab-case');
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!['pass', 'fail'].includes(status)) {
28
+ console.error(' ✗ status must be "pass" or "fail"');
29
+ process.exit(1);
30
+ }
31
+
32
+ if (!explanation) {
33
+ console.error(' ✗ explanation is required');
34
+ process.exit(1);
35
+ }
36
+
37
+ writeLesson(process.cwd(), slug, status, explanation);
38
+ console.log(`✓ lesson added: ${slug} (${status})`);
39
+ }
40
+
41
+ module.exports = lessonAtris;
@@ -29,7 +29,7 @@ function resolveSlug() {
29
29
  async function sleepAtris() {
30
30
  const slug = resolveSlug();
31
31
 
32
- if (!slug || slug === '--help') {
32
+ if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
33
33
  console.log('Usage: atris sleep [business]');
34
34
  console.log('');
35
35
  console.log(' Pause a workspace to save compute. Storage only.');
@@ -60,7 +60,7 @@ async function sleepAtris() {
60
60
  async function wakeAtris() {
61
61
  const slug = resolveSlug();
62
62
 
63
- if (!slug || slug === '--help') {
63
+ if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
64
64
  console.log('Usage: atris wake [business]');
65
65
  console.log('');
66
66
  console.log(' Wake a sleeping workspace. Agents resume automatically.');