atris 2.3.1 → 2.3.4

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.
@@ -1,6 +1,21 @@
1
- # brainstormer.md — Idea & Reality Shaper
1
+ ---
2
+ name: brainstormer
3
+ role: Idea Shaper
4
+ description: Shape ideas, explore possibilities, adapt to user depth
5
+ version: 1.0.0
6
+
7
+ skills: []
8
+
9
+ permissions:
10
+ can-read: true
11
+ can-plan: false
12
+ can-execute: false
13
+ can-approve: false
14
+ ---
15
+
16
+ # Brainstormer — Idea & Reality Shaper
2
17
 
3
- > **Role:** Shape ideas, explore possibilities, adapt to user depth | **Source:** Inbox items, raw ideas
18
+ > **Source:** Inbox items, raw ideas
4
19
  > **Style:** Read `atris/PERSONA.md` for communication style.
5
20
 
6
21
  ---
@@ -1,6 +1,21 @@
1
- # executor.md — Builder (The Trigger)
1
+ ---
2
+ name: executor
3
+ role: Builder
4
+ description: Execute from build specs, one step at a time
5
+ version: 1.0.0
6
+
7
+ skills: []
8
+
9
+ permissions:
10
+ can-read: true
11
+ can-plan: false
12
+ can-execute: true
13
+ can-approve: false
14
+ ---
15
+
16
+ # Executor — Builder
2
17
 
3
- > **Role:** Execute from build.md, one step at a time | **Source:** build.md, MAP.md
18
+ > **Source:** build.md, MAP.md
4
19
  > **Style:** Read `atris/PERSONA.md` for communication style.
5
20
 
6
21
  ---
@@ -1,6 +1,22 @@
1
- # launcher.md — The Closer
1
+ ---
2
+ name: launcher
3
+ role: Closer
4
+ description: Document, capture learnings, publish, celebrate
5
+ version: 1.0.0
6
+
7
+ skills: []
8
+
9
+ permissions:
10
+ can-read: true
11
+ can-plan: false
12
+ can-execute: false
13
+ can-approve: false
14
+ can-ship: true
15
+ ---
16
+
17
+ # Launcher — The Closer
2
18
 
3
- > **Role:** Document, capture learnings, publish, celebrate | **Source:** Completed tasks, validation results
19
+ > **Source:** Completed tasks, validation results
4
20
  > **Style:** Read `atris/PERSONA.md` for communication style.
5
21
 
6
22
  ---
@@ -1,6 +1,21 @@
1
- # navigator.md — Planner
1
+ ---
2
+ name: navigator
3
+ role: Planner
4
+ description: Transform messy human intent into precise execution plans
5
+ version: 1.0.0
6
+
7
+ skills: []
8
+
9
+ permissions:
10
+ can-read: true
11
+ can-plan: true
12
+ can-execute: false
13
+ can-approve: false
14
+ ---
15
+
16
+ # Navigator — Planner
2
17
 
3
- > **Role:** Transform messy human intent into precise execution plans | **Source:** idea.md, MAP.md
18
+ > **Source:** idea.md, MAP.md
4
19
  > **Style:** Read `atris/PERSONA.md` for communication style.
5
20
 
6
21
  ---
@@ -1,6 +1,22 @@
1
- # validator.md — Reviewer (The Safety)
1
+ ---
2
+ name: validator
3
+ role: Reviewer
4
+ description: Validate execution, run tests, ensure quality before shipping
5
+ version: 1.0.0
6
+
7
+ skills: []
8
+
9
+ permissions:
10
+ can-read: true
11
+ can-plan: false
12
+ can-execute: false
13
+ can-approve: true
14
+ can-ship: true
15
+ ---
16
+
17
+ # Validator — Reviewer
2
18
 
3
- > **Role:** Validate execution, update docs, ensure quality | **Source:** build.md, MAP.md, code
19
+ > **Source:** build.md, MAP.md, code
4
20
  > **Style:** Read `atris/PERSONA.md` for communication style.
5
21
 
6
22
  ---
package/bin/atris.js CHANGED
@@ -219,6 +219,8 @@ function showHelp() {
219
219
  console.log(' integrations - Show integration status');
220
220
  console.log('');
221
221
  console.log('Skills:');
222
+ console.log(' skill create <name> - Scaffold a new skill (--integration, --system)');
223
+ console.log(' skill link [--all] - Symlink skills to ~/.claude/skills/ (system-level)');
222
224
  console.log(' skill list - Show all skills with compliance status');
223
225
  console.log(' skill audit [name] - Validate skill against Anthropic guide');
224
226
  console.log(' skill fix [name] - Auto-fix common compliance issues');
@@ -897,31 +899,22 @@ function initAtris() {
897
899
  console.log('✓ Created TASK_CONTEXTS.md placeholder');
898
900
  }
899
901
 
900
- // Copy agent templates from package
901
- const navigatorSource = path.join(__dirname, '..', 'atris', 'team', 'navigator.md');
902
- const executorSource = path.join(__dirname, '..', 'atris', 'team', 'executor.md');
903
- const validatorSource = path.join(__dirname, '..', 'atris', 'team', 'validator.md');
904
- const launcherSource = path.join(__dirname, '..', 'atris', 'team', 'launcher.md');
902
+ // Copy agent templates from package (MEMBER.md directory format)
903
+ const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer'];
904
+ members.forEach(name => {
905
+ const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
906
+ const memberDir = path.join(teamDir, name);
907
+ const targetFile = path.join(memberDir, 'MEMBER.md');
908
+ const legacyFile = path.join(teamDir, `${name}.md`);
905
909
 
906
- if (!fs.existsSync(navigatorFile) && fs.existsSync(navigatorSource)) {
907
- fs.copyFileSync(navigatorSource, navigatorFile);
908
- console.log('✓ Created team/navigator.md');
909
- }
910
-
911
- if (!fs.existsSync(executorFile) && fs.existsSync(executorSource)) {
912
- fs.copyFileSync(executorSource, executorFile);
913
- console.log('✓ Created team/executor.md');
914
- }
915
-
916
- if (!fs.existsSync(validatorFile) && fs.existsSync(validatorSource)) {
917
- fs.copyFileSync(validatorSource, validatorFile);
918
- console.log('✓ Created team/validator.md');
919
- }
910
+ if (fs.existsSync(targetFile) || fs.existsSync(legacyFile)) return;
920
911
 
921
- if (!fs.existsSync(launcherFile) && fs.existsSync(launcherSource)) {
922
- fs.copyFileSync(launcherSource, launcherFile);
923
- console.log('✓ Created team/launcher.md');
924
- }
912
+ if (fs.existsSync(sourceFile)) {
913
+ fs.mkdirSync(memberDir, { recursive: true });
914
+ fs.copyFileSync(sourceFile, targetFile);
915
+ console.log(`✓ Created team/${name}/MEMBER.md`);
916
+ }
917
+ });
925
918
 
926
919
  // Copy policies from package
927
920
  const antislopSource = path.join(__dirname, '..', 'atris', 'policies', 'ANTISLOP.md');
@@ -987,10 +980,11 @@ function syncAtris() {
987
980
  { source: 'atrisDev.md', target: 'atrisDev.md' },
988
981
  { source: 'PERSONA.md', target: 'PERSONA.md' },
989
982
  { source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
990
- { source: 'atris/team/navigator.md', target: 'team/navigator.md' },
991
- { source: 'atris/team/executor.md', target: 'team/executor.md' },
992
- { source: 'atris/team/validator.md', target: 'team/validator.md' },
993
- { source: 'atris/team/launcher.md', target: 'team/launcher.md' },
983
+ { source: 'atris/team/navigator/MEMBER.md', target: 'team/navigator/MEMBER.md' },
984
+ { source: 'atris/team/executor/MEMBER.md', target: 'team/executor/MEMBER.md' },
985
+ { source: 'atris/team/validator/MEMBER.md', target: 'team/validator/MEMBER.md' },
986
+ { source: 'atris/team/launcher/MEMBER.md', target: 'team/launcher/MEMBER.md' },
987
+ { source: 'atris/team/brainstormer/MEMBER.md', target: 'team/brainstormer/MEMBER.md' },
994
988
  { source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
995
989
  ];
996
990
 
@@ -1014,6 +1008,7 @@ function syncAtris() {
1014
1008
  return;
1015
1009
  }
1016
1010
 
1011
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
1017
1012
  fs.copyFileSync(sourceFile, targetFile);
1018
1013
  console.log(`✓ Updated ${target}`);
1019
1014
  updated++;
package/commands/init.js CHANGED
@@ -475,36 +475,23 @@ function initAtris() {
475
475
  });
476
476
 
477
477
 
478
- const navigatorSource = path.join(__dirname, '..', 'atris', 'team', 'navigator.md');
479
- const executorSource = path.join(__dirname, '..', 'atris', 'team', 'executor.md');
480
- const validatorSource = path.join(__dirname, '..', 'atris', 'team', 'validator.md');
481
- const launcherSource = path.join(__dirname, '..', 'atris', 'team', 'launcher.md');
482
- const brainstormerSource = path.join(__dirname, '..', 'atris', 'team', 'brainstormer.md');
483
-
484
- if (!fs.existsSync(navigatorFile) && fs.existsSync(navigatorSource)) {
485
- fs.copyFileSync(navigatorSource, navigatorFile);
486
- console.log('✓ Created team/navigator.md');
487
- }
488
-
489
- if (!fs.existsSync(executorFile) && fs.existsSync(executorSource)) {
490
- fs.copyFileSync(executorSource, executorFile);
491
- console.log('✓ Created team/executor.md');
492
- }
493
-
494
- if (!fs.existsSync(validatorFile) && fs.existsSync(validatorSource)) {
495
- fs.copyFileSync(validatorSource, validatorFile);
496
- console.log('✓ Created team/validator.md');
497
- }
498
-
499
- if (!fs.existsSync(launcherFile) && fs.existsSync(launcherSource)) {
500
- fs.copyFileSync(launcherSource, launcherFile);
501
- console.log('✓ Created team/launcher.md');
502
- }
503
-
504
- if (!fs.existsSync(brainstormerFile) && fs.existsSync(brainstormerSource)) {
505
- fs.copyFileSync(brainstormerSource, brainstormerFile);
506
- console.log('✓ Created team/brainstormer.md');
507
- }
478
+ // Copy team members (MEMBER.md format directory per member)
479
+ const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer'];
480
+ members.forEach(name => {
481
+ const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
482
+ const targetDir = path.join(teamDir, name);
483
+ const targetFile = path.join(targetDir, 'MEMBER.md');
484
+ const legacyFile = path.join(teamDir, `${name}.md`);
485
+
486
+ // Skip if already exists (either format)
487
+ if (fs.existsSync(targetFile) || fs.existsSync(legacyFile)) return;
488
+
489
+ if (fs.existsSync(sourceFile)) {
490
+ fs.mkdirSync(targetDir, { recursive: true });
491
+ fs.copyFileSync(sourceFile, targetFile);
492
+ console.log(`✓ Created team/${name}/MEMBER.md`);
493
+ }
494
+ });
508
495
 
509
496
  // Detect project context and generate profile
510
497
  const profile = detectProjectContext(process.cwd());
package/commands/skill.js CHANGED
@@ -470,6 +470,377 @@ function skillFix(name) {
470
470
  console.log('');
471
471
  }
472
472
 
473
+ // --- Skill Scaffold Template ---
474
+
475
+ function generateSkillTemplate(name, description) {
476
+ return `---
477
+ name: ${name}
478
+ description: ${description || `Custom skill for ${name}. Use when user asks about ${name}-related tasks.`}
479
+ version: 1.0.0
480
+ tags:
481
+ - ${name}
482
+ ---
483
+
484
+ # ${name}
485
+
486
+ ## What This Skill Does
487
+
488
+ Describe what this skill does in 2-3 sentences.
489
+
490
+ ## Workflows
491
+
492
+ ### "Example trigger phrase"
493
+
494
+ 1. Step one
495
+ 2. Step two
496
+ 3. Step three
497
+
498
+ ## Rules
499
+
500
+ - Always confirm before taking destructive actions
501
+ - Never skip the approval gate on sends/deletes
502
+ `;
503
+ }
504
+
505
+ function generateIntegrationSkillTemplate(name, description) {
506
+ return `---
507
+ name: ${name}
508
+ description: ${description || `Integration skill for ${name}. Use when user asks about ${name}-related tasks.`}
509
+ version: 1.0.0
510
+ tags:
511
+ - ${name}
512
+ - integration
513
+ ---
514
+
515
+ # ${name}
516
+
517
+ ## Bootstrap (ALWAYS Run First)
518
+
519
+ Before any operation, run this bootstrap to ensure everything is set up:
520
+
521
+ \`\`\`bash
522
+ #!/bin/bash
523
+ set -e
524
+
525
+ # 1. Check if logged in to AtrisOS
526
+ if [ ! -f ~/.atris/credentials.json ]; then
527
+ echo "Not logged in to AtrisOS."
528
+ echo "Run: atris login"
529
+ exit 1
530
+ fi
531
+
532
+ # 2. Extract token
533
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
534
+
535
+ # 3. Check connection status
536
+ STATUS=$(curl -s "https://api.atris.ai/api/integrations/YOUR_INTEGRATION/status" \\
537
+ -H "Authorization: Bearer $TOKEN")
538
+
539
+ echo "$STATUS"
540
+ export ATRIS_TOKEN="$TOKEN"
541
+ \`\`\`
542
+
543
+ ## API Reference
544
+
545
+ Base: \`https://api.atris.ai/api/integrations/YOUR_INTEGRATION\`
546
+
547
+ All requests require: \`-H "Authorization: Bearer $TOKEN"\`
548
+
549
+ ### Get Token (after bootstrap)
550
+ \`\`\`bash
551
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
552
+ \`\`\`
553
+
554
+ ### List Items
555
+ \`\`\`bash
556
+ curl -s "https://api.atris.ai/api/integrations/YOUR_INTEGRATION/items" \\
557
+ -H "Authorization: Bearer $TOKEN"
558
+ \`\`\`
559
+
560
+ ## Workflows
561
+
562
+ ### "Example trigger phrase"
563
+
564
+ 1. Run bootstrap
565
+ 2. Call the API
566
+ 3. Display results
567
+ 4. **Confirm with user before any write action**
568
+
569
+ ## Error Handling
570
+
571
+ | Error | Meaning | Solution |
572
+ |-------|---------|----------|
573
+ | \`Token expired\` | AtrisOS session expired | Run \`atris login\` |
574
+ | \`401 Unauthorized\` | Invalid/expired token | Run \`atris login\` |
575
+ | \`429 Rate limited\` | Too many requests | Wait 60s, retry |
576
+
577
+ ## Security Model
578
+
579
+ 1. **Local token** (\`~/.atris/credentials.json\`): Stored locally with 600 permissions.
580
+ 2. **Integration credentials**: Stored server-side in AtrisOS encrypted vault. Never local.
581
+ 3. **HTTPS only**: All API communication encrypted in transit.
582
+ `;
583
+ }
584
+
585
+ // --- CREATE subcommand ---
586
+
587
+ function skillCreate(nameArg, ...flags) {
588
+ if (!nameArg) {
589
+ console.error('Usage: atris skill create <name> [--integration] [--description="..."] [--system]');
590
+ console.error('');
591
+ console.error('Examples:');
592
+ console.error(' atris skill create daily-standup');
593
+ console.error(' atris skill create email-outreach --integration');
594
+ console.error(' atris skill create pallet/bol-processor --integration');
595
+ console.error(' atris skill create my-skill --system');
596
+ process.exit(1);
597
+ }
598
+
599
+ const isIntegration = flags.includes('--integration');
600
+ const isLocal = flags.includes('--local');
601
+ const descFlag = flags.find(f => f.startsWith('--description='));
602
+ const description = descFlag ? descFlag.split('=').slice(1).join('=').replace(/^["']|["']$/g, '') : '';
603
+
604
+ // Parse name — supports "customer/skill-name" format
605
+ let skillDir, skillName, customerName;
606
+ if (nameArg.includes('/')) {
607
+ const parts = nameArg.split('/');
608
+ customerName = parts[0];
609
+ skillName = parts[1];
610
+ const customersDir = path.join(process.cwd(), 'atris', 'customers', customerName, 'skills');
611
+ skillDir = path.join(customersDir, skillName);
612
+ } else {
613
+ skillName = nameArg;
614
+ skillDir = path.join(process.cwd(), 'atris', 'skills', skillName);
615
+ }
616
+
617
+ // Check if already exists
618
+ if (fs.existsSync(skillDir)) {
619
+ console.error(`✗ Skill "${nameArg}" already exists at ${skillDir}`);
620
+ process.exit(1);
621
+ }
622
+
623
+ // Generate content
624
+ const content = isIntegration
625
+ ? generateIntegrationSkillTemplate(skillName, description)
626
+ : generateSkillTemplate(skillName, description);
627
+
628
+ // Create skill directory and SKILL.md
629
+ fs.mkdirSync(skillDir, { recursive: true });
630
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
631
+
632
+ console.log('');
633
+ if (customerName) {
634
+ console.log(`✓ Created atris/customers/${customerName}/skills/${skillName}/SKILL.md`);
635
+ } else {
636
+ console.log(`✓ Created atris/skills/${skillName}/SKILL.md`);
637
+ }
638
+
639
+ // Symlink to project-level .claude/skills/
640
+ const projectClaudeSkills = path.join(process.cwd(), '.claude', 'skills');
641
+ if (fs.existsSync(path.join(process.cwd(), '.claude'))) {
642
+ fs.mkdirSync(projectClaudeSkills, { recursive: true });
643
+ const projectLink = path.join(projectClaudeSkills, skillName);
644
+ if (!fs.existsSync(projectLink)) {
645
+ try {
646
+ const relTarget = path.relative(projectClaudeSkills, skillDir);
647
+ fs.symlinkSync(relTarget, projectLink);
648
+ console.log(`✓ Linked .claude/skills/${skillName} (project-level)`);
649
+ } catch (e) {
650
+ // Copy fallback
651
+ fs.mkdirSync(projectLink, { recursive: true });
652
+ fs.copyFileSync(path.join(skillDir, 'SKILL.md'), path.join(projectLink, 'SKILL.md'));
653
+ console.log(`✓ Copied to .claude/skills/${skillName} (project-level)`);
654
+ }
655
+ }
656
+ }
657
+
658
+ // Symlink to all AI tool skill directories (always, unless --local)
659
+ if (!isLocal) {
660
+ const home = require('os').homedir();
661
+ const toolDirs = [
662
+ { dir: path.join(home, '.claude', 'skills'), label: 'Claude' },
663
+ { dir: path.join(home, '.codex', 'skills'), label: 'Codex' },
664
+ { dir: path.join(home, '.cursor', 'skills'), label: 'Cursor' },
665
+ ];
666
+
667
+ const linked = [];
668
+ for (const { dir, label } of toolDirs) {
669
+ fs.mkdirSync(dir, { recursive: true });
670
+ const linkPath = path.join(dir, skillName);
671
+ if (!fs.existsSync(linkPath)) {
672
+ try {
673
+ fs.symlinkSync(skillDir, linkPath);
674
+ linked.push(label);
675
+ } catch (e) {
676
+ // Copy fallback
677
+ fs.mkdirSync(linkPath, { recursive: true });
678
+ fs.copyFileSync(path.join(skillDir, 'SKILL.md'), path.join(linkPath, 'SKILL.md'));
679
+ linked.push(label);
680
+ }
681
+ }
682
+ }
683
+ if (linked.length > 0) {
684
+ console.log(`✓ Linked to ${linked.join(', ')} (system-level — all tools)`);
685
+ }
686
+ }
687
+
688
+ // Summary
689
+ console.log('');
690
+ if (isIntegration) {
691
+ console.log(' Template: integration (bootstrap + API reference + error handling)');
692
+ } else {
693
+ console.log(' Template: standard (workflows + rules)');
694
+ }
695
+ console.log(` Edit: ${path.join(skillDir, 'SKILL.md')}`);
696
+ console.log('');
697
+ }
698
+
699
+ // --- LINK subcommand (system-level symlink for existing skills) ---
700
+
701
+ function skillLink(name, ...flags) {
702
+ const isAll = name === '--all';
703
+
704
+ const skillsDir = path.join(process.cwd(), 'atris', 'skills');
705
+ const allSkills = findAllSkills(skillsDir);
706
+
707
+ if (allSkills.length === 0) {
708
+ console.error('No skills found in atris/skills/.');
709
+ process.exit(1);
710
+ }
711
+
712
+ const targets = isAll
713
+ ? allSkills
714
+ : allSkills.filter(s => s.folder === name || s.leafFolder === name);
715
+
716
+ if (targets.length === 0) {
717
+ console.error(`Skill "${name}" not found. Run "atris skill list".`);
718
+ process.exit(1);
719
+ }
720
+
721
+ const home = require('os').homedir();
722
+ const toolDirs = [
723
+ { dir: path.join(home, '.claude', 'skills'), label: 'Claude' },
724
+ { dir: path.join(home, '.codex', 'skills'), label: 'Codex' },
725
+ { dir: path.join(home, '.cursor', 'skills'), label: 'Cursor' },
726
+ ];
727
+
728
+ // Ensure all tool directories exist
729
+ for (const { dir } of toolDirs) {
730
+ fs.mkdirSync(dir, { recursive: true });
731
+ }
732
+
733
+ let linked = 0;
734
+ for (const skill of targets) {
735
+ const srcDir = path.dirname(skill.path);
736
+ const linkName = skill.leafFolder;
737
+ const toolsLinked = [];
738
+
739
+ for (const { dir, label } of toolDirs) {
740
+ const linkPath = path.join(dir, linkName);
741
+
742
+ if (fs.existsSync(linkPath)) {
743
+ try {
744
+ const existing = fs.readlinkSync(linkPath);
745
+ if (existing === srcDir || path.resolve(linkPath, '..', existing) === srcDir) {
746
+ continue; // Already linked correctly
747
+ }
748
+ } catch (e) {
749
+ continue; // Not a symlink, skip
750
+ }
751
+ }
752
+
753
+ try {
754
+ fs.symlinkSync(srcDir, linkPath);
755
+ toolsLinked.push(label);
756
+ } catch (e) {
757
+ // silent fail per tool
758
+ }
759
+ }
760
+
761
+ if (toolsLinked.length > 0) {
762
+ console.log(`✓ ${linkName} → ${toolsLinked.join(', ')}`);
763
+ linked++;
764
+ }
765
+ }
766
+
767
+ if (linked === 0) {
768
+ console.log('All skills already linked at system level.');
769
+ } else {
770
+ console.log(`\n${linked} skill(s) linked to ~/.claude/skills/ (available in all tools).`);
771
+ }
772
+ }
773
+
774
+ // --- DELETE subcommand ---
775
+
776
+ function skillDelete(name) {
777
+ if (!name) {
778
+ console.error('Usage: atris skill delete <name>');
779
+ process.exit(1);
780
+ }
781
+
782
+ const home = require('os').homedir();
783
+ const removed = [];
784
+
785
+ // Remove from atris/skills/
786
+ const skillDir = path.join(process.cwd(), 'atris', 'skills', name);
787
+ if (fs.existsSync(skillDir)) {
788
+ fs.rmSync(skillDir, { recursive: true, force: true });
789
+ removed.push(`atris/skills/${name}`);
790
+ }
791
+
792
+ // Remove from atris/customers/ (check all customers)
793
+ const customersDir = path.join(process.cwd(), 'atris', 'customers');
794
+ if (fs.existsSync(customersDir)) {
795
+ const customers = fs.readdirSync(customersDir);
796
+ for (const customer of customers) {
797
+ const custSkillDir = path.join(customersDir, customer, 'skills', name);
798
+ if (fs.existsSync(custSkillDir)) {
799
+ fs.rmSync(custSkillDir, { recursive: true, force: true });
800
+ removed.push(`atris/customers/${customer}/skills/${name}`);
801
+ }
802
+ }
803
+ }
804
+
805
+ // Remove symlinks — use unlinkSync for symlinks, rmSync for directories
806
+ function removeLink(linkPath, label) {
807
+ try {
808
+ const stat = fs.lstatSync(linkPath);
809
+ if (stat.isSymbolicLink()) {
810
+ fs.unlinkSync(linkPath);
811
+ } else {
812
+ fs.rmSync(linkPath, { recursive: true, force: true });
813
+ }
814
+ removed.push(label);
815
+ } catch (e) { /* doesn't exist */ }
816
+ }
817
+
818
+ // Project-level
819
+ removeLink(path.join(process.cwd(), '.claude', 'skills', name), `.claude/skills/${name}`);
820
+
821
+ // System-level — all tool directories
822
+ const toolDirs = [
823
+ { dir: path.join(home, '.claude', 'skills', name), label: '~/.claude' },
824
+ { dir: path.join(home, '.codex', 'skills', name), label: '~/.codex' },
825
+ { dir: path.join(home, '.cursor', 'skills', name), label: '~/.cursor' },
826
+ ];
827
+
828
+ for (const { dir, label } of toolDirs) {
829
+ removeLink(dir, `${label}/skills/${name}`);
830
+ }
831
+
832
+ if (removed.length === 0) {
833
+ console.error(`✗ Skill "${name}" not found anywhere.`);
834
+ process.exit(1);
835
+ }
836
+
837
+ console.log('');
838
+ for (const r of removed) {
839
+ console.log(`✓ Removed ${r}`);
840
+ }
841
+ console.log('');
842
+ }
843
+
473
844
  // --- Main Dispatcher ---
474
845
 
475
846
  function skillCommand(subcommand, ...args) {
@@ -481,15 +852,38 @@ function skillCommand(subcommand, ...args) {
481
852
  return skillAudit(args[0] || '--all');
482
853
  case 'fix':
483
854
  return skillFix(args[0] || '--all');
855
+ case 'create':
856
+ case 'new':
857
+ return skillCreate(args[0], ...args.slice(1));
858
+ case 'link':
859
+ return skillLink(args[0] || '--all', ...args.slice(1));
860
+ case 'delete':
861
+ case 'rm':
862
+ case 'remove':
863
+ return skillDelete(args[0]);
484
864
  default:
485
865
  console.log('');
486
866
  console.log('Usage: atris skill <subcommand> [name]');
487
867
  console.log('');
488
868
  console.log('Subcommands:');
489
- console.log(' list Show all skills with compliance status');
869
+ console.log(' create <name> Scaffold a new skill with SKILL.md template');
870
+ console.log(' delete <name> Remove a skill and all its symlinks');
871
+ console.log(' link [name|--all] Symlink skills to ~/.claude/skills/ (system-level)');
872
+ console.log(' list Show all skills with compliance status');
490
873
  console.log(' audit [name|--all] Validate skill against Anthropic guide');
491
874
  console.log(' fix [name|--all] Auto-fix common compliance issues');
492
875
  console.log('');
876
+ console.log('Create flags:');
877
+ console.log(' --integration Use integration template (bootstrap + API)');
878
+ console.log(' --local Only link to this project (skip system-level)');
879
+ console.log(' --description="..." Set the skill description');
880
+ console.log('');
881
+ console.log('Examples:');
882
+ console.log(' atris skill create daily-standup');
883
+ console.log(' atris skill create email-outreach --integration');
884
+ console.log(' atris skill create pallet/bol-processor --integration');
885
+ console.log(' atris skill link --all');
886
+ console.log('');
493
887
  }
494
888
  }
495
889
 
package/commands/sync.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const os = require('os');
3
4
 
4
5
  function syncAtris() {
5
6
  const targetDir = path.join(process.cwd(), 'atris');
@@ -59,11 +60,11 @@ function syncAtris() {
59
60
  { source: 'PERSONA.md', target: 'PERSONA.md' },
60
61
  { source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
61
62
  { source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
62
- { source: 'atris/team/navigator.md', target: 'team/navigator.md' },
63
- { source: 'atris/team/executor.md', target: 'team/executor.md' },
64
- { source: 'atris/team/validator.md', target: 'team/validator.md' },
65
- { source: 'atris/team/launcher.md', target: 'team/launcher.md' },
66
- { source: 'atris/team/brainstormer.md', target: 'team/brainstormer.md' },
63
+ { source: 'atris/team/navigator/MEMBER.md', target: 'team/navigator/MEMBER.md' },
64
+ { source: 'atris/team/executor/MEMBER.md', target: 'team/executor/MEMBER.md' },
65
+ { source: 'atris/team/validator/MEMBER.md', target: 'team/validator/MEMBER.md' },
66
+ { source: 'atris/team/launcher/MEMBER.md', target: 'team/launcher/MEMBER.md' },
67
+ { source: 'atris/team/brainstormer/MEMBER.md', target: 'team/brainstormer/MEMBER.md' },
67
68
  { source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
68
69
  ];
69
70
 
@@ -87,6 +88,7 @@ function syncAtris() {
87
88
  return;
88
89
  }
89
90
 
91
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
90
92
  fs.copyFileSync(sourceFile, targetFile);
91
93
  console.log(`✓ Updated ${target}`);
92
94
  updated++;
@@ -293,83 +295,114 @@ After displaying the boot output, respond to the user naturally.
293
295
  }
294
296
 
295
297
  /**
296
- * Lightweight skill-only sync. Compares package skills against project skills
297
- * and updates any that differ. Called automatically from `atris activate`.
298
+ * Recursively sync files from src to dest. Returns count of files updated.
299
+ */
300
+ function syncRecursiveCount(src, dest, label, silent) {
301
+ let count = 0;
302
+ if (!fs.existsSync(dest)) {
303
+ fs.mkdirSync(dest, { recursive: true });
304
+ }
305
+ const entries = fs.readdirSync(src);
306
+ for (const entry of entries) {
307
+ const srcPath = path.join(src, entry);
308
+ const destPath = path.join(dest, entry);
309
+
310
+ if (fs.statSync(srcPath).isDirectory()) {
311
+ count += syncRecursiveCount(srcPath, destPath, `${label}/${entry}`, silent);
312
+ } else {
313
+ const srcContent = fs.readFileSync(srcPath, 'utf8');
314
+ const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
315
+ if (srcContent !== destContent) {
316
+ fs.writeFileSync(destPath, srcContent);
317
+ if (entry.endsWith('.sh')) {
318
+ fs.chmodSync(destPath, 0o755);
319
+ }
320
+ if (!silent) {
321
+ console.log(`✓ Updated ${label}/${entry}`);
322
+ }
323
+ count++;
324
+ }
325
+ }
326
+ }
327
+ return count;
328
+ }
329
+
330
+ /**
331
+ * Lightweight skill-only sync. Syncs skills from the npm package to:
332
+ * 1. Global skill dirs (~/.claude/skills/, ~/.codex/skills/) — always, if they exist
333
+ * 2. Project-level (atris/skills/ + .claude/skills/ symlinks) — if in a project
334
+ *
335
+ * Global = baseline truth. Project = optional override.
298
336
  * Returns number of files updated (0 = already current).
299
337
  */
300
338
  function syncSkills({ silent = false } = {}) {
301
- const targetDir = path.join(process.cwd(), 'atris');
302
339
  const packageSkillsDir = path.join(__dirname, '..', 'atris', 'skills');
303
- const userSkillsDir = path.join(targetDir, 'skills');
304
- const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
305
-
306
- if (!fs.existsSync(targetDir) || !fs.existsSync(packageSkillsDir)) {
340
+ if (!fs.existsSync(packageSkillsDir)) {
307
341
  return 0;
308
342
  }
309
343
 
310
- if (!fs.existsSync(userSkillsDir)) {
311
- fs.mkdirSync(userSkillsDir, { recursive: true });
312
- }
313
- if (!fs.existsSync(claudeSkillsBaseDir)) {
314
- fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
315
- }
316
-
317
344
  let updated = 0;
345
+ const homeDir = os.homedir();
318
346
 
319
347
  const skillFolders = fs.readdirSync(packageSkillsDir).filter(f =>
320
348
  fs.statSync(path.join(packageSkillsDir, f)).isDirectory()
321
349
  );
322
350
 
323
- for (const skill of skillFolders) {
324
- const srcSkillDir = path.join(packageSkillsDir, skill);
325
- const destSkillDir = path.join(userSkillsDir, skill);
326
- const symlinkPath = path.join(claudeSkillsBaseDir, skill);
351
+ // --- 1. Global skill directories (sync if they exist) ---
352
+ const globalSkillDirs = [
353
+ path.join(homeDir, '.claude', 'skills'),
354
+ path.join(homeDir, '.codex', 'skills'),
355
+ ];
327
356
 
328
- const syncRecursive = (src, dest, skillName, basePath = '') => {
329
- if (!fs.existsSync(dest)) {
330
- fs.mkdirSync(dest, { recursive: true });
331
- }
332
- const entries = fs.readdirSync(src);
333
- for (const entry of entries) {
334
- const srcPath = path.join(src, entry);
335
- const destPath = path.join(dest, entry);
336
- const relPath = basePath ? `${basePath}/${entry}` : entry;
337
-
338
- if (fs.statSync(srcPath).isDirectory()) {
339
- syncRecursive(srcPath, destPath, skillName, relPath);
340
- } else {
341
- const srcContent = fs.readFileSync(srcPath, 'utf8');
342
- const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
343
- if (srcContent !== destContent) {
344
- fs.writeFileSync(destPath, srcContent);
345
- if (entry.endsWith('.sh')) {
346
- fs.chmodSync(destPath, 0o755);
347
- }
348
- if (!silent) {
349
- console.log(`✓ Updated atris/skills/${skillName}/${relPath}`);
350
- }
351
- updated++;
352
- }
353
- }
357
+ for (const globalDir of globalSkillDirs) {
358
+ if (!fs.existsSync(globalDir)) continue;
359
+ const dirName = path.basename(path.dirname(globalDir)); // .claude or .codex
360
+
361
+ for (const skill of skillFolders) {
362
+ const srcSkillDir = path.join(packageSkillsDir, skill);
363
+ const destSkillDir = path.join(globalDir, skill);
364
+
365
+ if (fs.existsSync(destSkillDir) || fs.existsSync(globalDir)) {
366
+ updated += syncRecursiveCount(srcSkillDir, destSkillDir, `~/${dirName}/skills/${skill}`, silent);
354
367
  }
355
- };
368
+ }
369
+ }
356
370
 
357
- syncRecursive(srcSkillDir, destSkillDir, skill);
371
+ // --- 2. Project-level (only if inside an atris project) ---
372
+ const targetDir = path.join(process.cwd(), 'atris');
373
+ if (fs.existsSync(targetDir)) {
374
+ const userSkillsDir = path.join(targetDir, 'skills');
375
+ const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
358
376
 
359
- // Create symlink if doesn't exist
360
- if (!fs.existsSync(symlinkPath)) {
361
- const relativePath = path.join('..', '..', 'atris', 'skills', skill);
362
- try {
363
- fs.symlinkSync(relativePath, symlinkPath);
364
- if (!silent) {
365
- console.log(`✓ Linked .claude/skills/${skill}`);
366
- }
367
- } catch (e) {
368
- // Fallback: copy instead of symlink
369
- fs.mkdirSync(symlinkPath, { recursive: true });
370
- const skillFile = path.join(destSkillDir, 'SKILL.md');
371
- if (fs.existsSync(skillFile)) {
372
- fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
377
+ if (!fs.existsSync(userSkillsDir)) {
378
+ fs.mkdirSync(userSkillsDir, { recursive: true });
379
+ }
380
+ if (!fs.existsSync(claudeSkillsBaseDir)) {
381
+ fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
382
+ }
383
+
384
+ for (const skill of skillFolders) {
385
+ const srcSkillDir = path.join(packageSkillsDir, skill);
386
+ const destSkillDir = path.join(userSkillsDir, skill);
387
+ const symlinkPath = path.join(claudeSkillsBaseDir, skill);
388
+
389
+ updated += syncRecursiveCount(srcSkillDir, destSkillDir, `atris/skills/${skill}`, silent);
390
+
391
+ // Create symlink if doesn't exist
392
+ if (!fs.existsSync(symlinkPath)) {
393
+ const relativePath = path.join('..', '..', 'atris', 'skills', skill);
394
+ try {
395
+ fs.symlinkSync(relativePath, symlinkPath);
396
+ if (!silent) {
397
+ console.log(`✓ Linked .claude/skills/${skill}`);
398
+ }
399
+ } catch (e) {
400
+ // Fallback: copy instead of symlink
401
+ fs.mkdirSync(symlinkPath, { recursive: true });
402
+ const skillFile = path.join(destSkillDir, 'SKILL.md');
403
+ if (fs.existsSync(skillFile)) {
404
+ fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
405
+ }
373
406
  }
374
407
  }
375
408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "2.3.1",
3
+ "version": "2.3.4",
4
4
  "description": "atrisDev (atris dev) - CLI for AI coding agents. Works with Claude Code, Cursor, Windsurf. Make any codebase AI-navigable.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {