@inkobytes/nexus 1.0.2 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 1.0.2 - 2026-06-03
3
+ ## 1.0.4 - 2026-06-03
4
4
 
5
5
  - Collapsed large `nexus doctor` Git Privacy floods into grouped per-root summaries with sample paths.
6
6
  - Prioritized `CONTINUITY.md` and `memories/` samples first so agent-local issues stay visible.
7
+ - Collapsed shared agent roots like `.claude/`, `.codex/`, and `.gemini/` into one concise Git Privacy note for repos that intentionally track them.
8
+ - Colorized `nexus doctor` output and grouped findings under clearer action buckets like `Fix the following` and `Review / informational`.
9
+ - Added `.nexus/config.json` support for `doctor.allowTrackedAgentTrees` so intentionally tracked shared agent trees can be treated as informational.
10
+ - Reformatted lock and queue findings into compact field-style output like `file`, `by`, `needs`, `impact`, and shorter `fix` lines.
7
11
 
8
12
  ## 1.0.1 - 2026-06-02
9
13
 
package/README.md CHANGED
@@ -166,7 +166,8 @@ Doctor reports grouped issues:
166
166
  - missing Nexus files
167
167
  - package script exfiltration and install-hook risks
168
168
  - package privacy risks for local/private files
169
- - grouped Git Privacy summaries for tracked private/local trees so large agent folders stay readable
169
+ - grouped Git Privacy summaries for tracked private/local trees, with shared agent dirs collapsed into one concise note
170
+ - colorized action buckets so fixes and informational lock notes are easier to scan
170
171
  - stale nexus locks
171
172
  - missing agent instructions specifically for nexus
172
173
  - missing continuity and memory scaffolds
@@ -176,6 +177,18 @@ With `--fix`, Nexus creates safe missing scaffolds and updates managed protocol
176
177
 
177
178
  With `--json`, Nexus prints the same health sections as structured JSON for tools such as Inkobytes reports.
178
179
 
180
+ If a private repo intentionally tracks shared agent trees like `.claude/`, `.codex/`, or `.gemini/`, you can mark that as allowed in `.nexus/config.json`:
181
+
182
+ ```json
183
+ {
184
+ "doctor": {
185
+ "allowTrackedAgentTrees": true
186
+ }
187
+ }
188
+ ```
189
+
190
+ With that setting, `nexus doctor` keeps the shared-agent-tree note as informational instead of repeating an untrack fix.
191
+
179
192
  Use `doctor` for audit or repair. Do not make it the normal first command for every agent session.
180
193
 
181
194
  ### `nexus soul [--file <path>] [--status | --remove]`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkobytes/nexus",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Multi-agent coordination CLI for coding agents sharing a local repository",
5
5
  "type": "module",
6
6
  "bin": {
@@ -347,6 +347,7 @@ export default function doctor(args) {
347
347
  const fix = args.includes('--fix');
348
348
  const json = args.includes('--json');
349
349
  const root = cwd();
350
+ const colors = createColors();
350
351
  const sections = {
351
352
  'Nexus Files': [],
352
353
  'Agent Instructions': [],
@@ -365,8 +366,8 @@ export default function doctor(args) {
365
366
  const config = getConfig(root);
366
367
 
367
368
  if (!json) {
368
- console.log(`Nexus doctor${fix ? ' --fix' : ''}`);
369
- console.log(`Repo: ${root}\n`);
369
+ console.log(colors.bold(colors.cyan(`Nexus doctor${fix ? ' --fix' : ''}`)));
370
+ console.log(`${colors.dim('Repo:')} ${root}\n`);
370
371
  }
371
372
 
372
373
  const nexusProtocolFiles = ['_NEXUS_CONSTITUTION.md', '_NEXUS_QUEUE.md', '_NEXUS_STANDUP.md'];
@@ -412,7 +413,7 @@ export default function doctor(args) {
412
413
  sections['Package Privacy'].push(issue);
413
414
  }
414
415
 
415
- for (const issue of scanGitPrivacy(root)) {
416
+ for (const issue of scanGitPrivacy(root, config)) {
416
417
  sections['Git Privacy'].push(issue);
417
418
  }
418
419
 
@@ -520,6 +521,13 @@ export default function doctor(args) {
520
521
  sections.Locks.push({
521
522
  issue: `Stale lock on ${lock.target} (${lock.age}s old)`,
522
523
  fix: 'Run `nexus clean --stale`.',
524
+ displayGroup: lock.target,
525
+ lockInfo: {
526
+ target: lock.target,
527
+ agent: lock.agent || '',
528
+ kind: 'stale',
529
+ age: `${lock.age}s old`,
530
+ },
523
531
  });
524
532
  }
525
533
  }
@@ -531,18 +539,38 @@ export default function doctor(args) {
531
539
  issue: `Active lock on ${lock.target} (${age})`,
532
540
  fix: 'No action if the agent is still working. Use `nexus status` to inspect.',
533
541
  ok: true,
542
+ displayGroup: lock.target,
543
+ lockInfo: {
544
+ target: lock.target,
545
+ agent: lock.agent || '',
546
+ kind: 'active',
547
+ age,
548
+ },
534
549
  });
535
550
  if (!lock.model) {
536
551
  sections.Locks.push({
537
552
  issue: `Active lock on ${lock.target} has no --model metadata`,
538
553
  fix: 'Use `nexus claim ... --model <name>` for future claims; only the human operator can declare the real model.',
539
554
  ok: true,
555
+ displayGroup: lock.target,
556
+ lockInfo: {
557
+ target: lock.target,
558
+ agent: lock.agent || '',
559
+ kind: 'missing_model',
560
+ },
540
561
  });
541
562
  }
542
563
  if (!lock.verified) {
543
564
  sections.Locks.push({
544
565
  issue: `Unverified claim on ${lock.target} by ${lock.agent} (trust: ${lock.trustSource}) — no CLAUDECODE or NEXUS_AGENT env detected at claim time`,
545
566
  fix: 'If this is a local/unverified model, set NEXUS_AGENT=@handle before claiming. If unexpected, inspect the lock.',
567
+ displayGroup: lock.target,
568
+ lockInfo: {
569
+ target: lock.target,
570
+ agent: lock.agent || '',
571
+ kind: 'unverified',
572
+ trustSource: lock.trustSource,
573
+ },
546
574
  });
547
575
  }
548
576
  }
@@ -611,8 +639,15 @@ export default function doctor(args) {
611
639
  if (unapproved.length) {
612
640
  for (const id of unapproved) {
613
641
  sections['Queue Authorship'].push({
614
- issue: `Task "${id}" is auto-flow: yes in Ready Queue but missing Review: approved — nexus next will skip it`,
615
- fix: 'Add "- Review: approved" and "- Approved by: human" to the task, or move it to ## Proposed Queue.',
642
+ issue: `Task "${id}" is missing Review: approved`,
643
+ fix: 'add `Review: approved` and `Approved by: human`, or move it to `## Proposed Queue`',
644
+ displayGroup: id,
645
+ queueInfo: {
646
+ taskId: id,
647
+ state: 'auto-flow: yes in Ready Queue',
648
+ needs: 'Review: approved',
649
+ impact: 'nexus next will skip it',
650
+ },
616
651
  });
617
652
  }
618
653
  } else {
@@ -658,40 +693,193 @@ export default function doctor(args) {
658
693
  }
659
694
 
660
695
  if (changes.length) {
661
- console.log('Applied fixes:');
662
- for (const change of changes) console.log(` - ${change}`);
696
+ console.log(colors.bold(colors.green('Applied fixes')));
697
+ for (const change of changes) console.log(` ${colors.green('-')} ${change}`);
663
698
  console.log('');
664
699
  }
665
700
 
666
701
  let problemCount = 0;
667
702
  for (const [title, entries] of Object.entries(sections)) {
668
- console.log(`[${title}]`);
703
+ console.log(colors.bold(colors.cyan(`[${title}]`)));
669
704
  if (!entries.length) {
670
- console.log(' OK');
705
+ console.log(` ${colors.green('OK')}`);
671
706
  console.log('');
672
707
  continue;
673
708
  }
674
709
 
675
- for (const entry of entries) {
710
+ const actionable = entries.filter((entry) => !entry.ok);
711
+ const informational = entries.filter((entry) => entry.ok);
712
+ problemCount += actionable.length;
713
+
714
+ if (actionable.length) renderEntryBucket('Fix the following', actionable, colors.yellow, colors.red, title, colors);
715
+ if (informational.length) renderEntryBucket('Review / informational', informational, colors.blue, colors.green, title, colors);
716
+ console.log('');
717
+ }
718
+
719
+ if (problemCount) {
720
+ console.log(colors.bold(colors.yellow('Some issues need attention.')));
721
+ console.log(colors.dim('Safe scaffold fixes: `nexus doctor --fix`.'));
722
+ return;
723
+ }
724
+
725
+ console.log(colors.bold(colors.green('All checked Nexus categories are ready.')));
726
+ }
727
+
728
+ function renderEntryBucket(label, entries, headingColor, markerColor, sectionTitle, colors) {
729
+ console.log(` ${headingColor(label)}`);
730
+ if (sectionTitle === 'Locks') {
731
+ renderLockEntries(entries, markerColor, colors);
732
+ return;
733
+ }
734
+ if (sectionTitle === 'Queue Authorship') {
735
+ renderQueueEntries(entries, markerColor, colors);
736
+ return;
737
+ }
738
+ const groups = groupEntriesForDisplay(entries, sectionTitle);
739
+ for (const group of groups) {
740
+ if (group.label) {
741
+ console.log(` ${colors.bold(group.label)}`);
742
+ }
743
+ for (const entry of group.entries) {
744
+ const baseIndent = group.label ? ' ' : ' ';
745
+ const detailIndent = group.label ? ' ' : ' ';
676
746
  const prefix = entry.ok ? '-' : '!';
677
- console.log(` ${prefix} ${entry.issue}`);
747
+ console.log(`${baseIndent}${markerColor(prefix)} ${entry.issue}`);
678
748
  if (entry.details) {
679
749
  for (const detail of entry.details) {
680
- console.log(` ${detail}`);
750
+ console.log(`${detailIndent}${colors.dim(detail)}`);
681
751
  }
682
752
  }
683
- if (entry.fix) console.log(` Fix: ${entry.fix}`);
684
- if (!entry.ok) problemCount++;
753
+ if (entry.fix) {
754
+ console.log(`${detailIndent}${colors.bold('Fix:')} ${colors.dim(entry.fix)}`);
755
+ }
685
756
  }
686
- console.log('');
687
757
  }
758
+ }
688
759
 
689
- if (problemCount) {
690
- console.log('Some issues need attention. Safe scaffold fixes: `nexus doctor --fix`.');
691
- return;
760
+ function renderLockEntries(entries, markerColor, colors) {
761
+ const groups = new Map();
762
+
763
+ for (const entry of entries) {
764
+ const target = entry.lockInfo?.target || entry.displayGroup || entry.issue;
765
+ if (!groups.has(target)) groups.set(target, []);
766
+ groups.get(target).push(entry);
767
+ }
768
+
769
+ for (const [target, lockEntries] of groups) {
770
+ const agent = lockEntries.find((entry) => entry.lockInfo?.agent)?.lockInfo?.agent || '';
771
+ console.log(` ${colors.bold(`file: ${target}`)}`);
772
+ console.log(` ${colors.dim(`by: ${agent || 'unknown'}`)}`);
773
+ for (const entry of lockEntries) {
774
+ const prefix = entry.ok ? '-' : '!';
775
+ const state = formatLockState(entry);
776
+ console.log(` ${markerColor(prefix)} ${state}`);
777
+ if (entry.fix) {
778
+ console.log(` ${colors.bold('fix:')} ${colors.dim(compactLockFix(entry))}`);
779
+ }
780
+ }
781
+ }
782
+ }
783
+
784
+ function renderQueueEntries(entries, markerColor, colors) {
785
+ const groups = new Map();
786
+
787
+ for (const entry of entries) {
788
+ const taskId = entry.queueInfo?.taskId || entry.displayGroup || entry.issue;
789
+ if (!groups.has(taskId)) groups.set(taskId, []);
790
+ groups.get(taskId).push(entry);
791
+ }
792
+
793
+ for (const [taskId, taskEntries] of groups) {
794
+ console.log(` ${colors.bold(`task: ${taskId}`)}`);
795
+ for (const entry of taskEntries) {
796
+ const prefix = entry.ok ? '-' : '!';
797
+ const state = entry.queueInfo?.state || entry.issue;
798
+ const needs = entry.queueInfo?.needs;
799
+ const impact = entry.queueInfo?.impact;
800
+ console.log(` ${markerColor(prefix)} ${state}`);
801
+ if (needs) {
802
+ console.log(` ${colors.dim(`needs: ${needs}`)}`);
803
+ }
804
+ if (impact) {
805
+ console.log(` ${colors.dim(`impact: ${impact}`)}`);
806
+ }
807
+ if (entry.fix) {
808
+ console.log(` ${colors.bold('fix:')} ${colors.dim(entry.fix)}`);
809
+ }
810
+ }
692
811
  }
812
+ }
693
813
 
694
- console.log('All checked Nexus categories are ready.');
814
+ function formatLockState(entry) {
815
+ const info = entry.lockInfo;
816
+ if (!info) return entry.issue;
817
+
818
+ switch (info.kind) {
819
+ case 'stale':
820
+ return `stale lock (${info.age})`;
821
+ case 'active':
822
+ return `active lock (${info.age})`;
823
+ case 'missing_model':
824
+ return 'missing --model metadata';
825
+ case 'unverified':
826
+ return `unverified claim (trust: ${info.trustSource || 'unknown'})`;
827
+ default:
828
+ return entry.issue;
829
+ }
830
+ }
831
+
832
+ function compactLockFix(entry) {
833
+ const info = entry.lockInfo;
834
+ if (!info) return entry.fix;
835
+
836
+ switch (info.kind) {
837
+ case 'stale':
838
+ return 'run `nexus clean --stale`';
839
+ case 'active':
840
+ return 'leave it if someone is working, or inspect with `nexus status`';
841
+ case 'missing_model':
842
+ return 'use `nexus claim ... --model <name>` on future claims';
843
+ case 'unverified':
844
+ return 'set `NEXUS_AGENT=@handle` for local claims, or inspect the lock';
845
+ default:
846
+ return entry.fix;
847
+ }
848
+ }
849
+
850
+ function groupEntriesForDisplay(entries, sectionTitle) {
851
+ const groups = [];
852
+ const grouped = new Map();
853
+
854
+ for (const entry of entries) {
855
+ const label = entry.displayGroup || inferDisplayGroup(entry.issue, sectionTitle);
856
+ if (!label) {
857
+ groups.push({ label: '', entries: [entry] });
858
+ continue;
859
+ }
860
+ if (!grouped.has(label)) {
861
+ const group = { label, entries: [] };
862
+ grouped.set(label, group);
863
+ groups.push(group);
864
+ }
865
+ grouped.get(label).entries.push(entry);
866
+ }
867
+
868
+ return groups;
869
+ }
870
+
871
+ function inferDisplayGroup(issue, sectionTitle) {
872
+ if (sectionTitle === 'Locks') {
873
+ const lockMatch = issue.match(/^(?:Stale lock on|Active lock on|Unverified claim on) ([^ ]+)/);
874
+ if (lockMatch) return lockMatch[1];
875
+ }
876
+
877
+ if (sectionTitle === 'Queue Authorship') {
878
+ const taskMatch = issue.match(/^Task "([^"]+)"/);
879
+ if (taskMatch) return taskMatch[1];
880
+ }
881
+
882
+ return '';
695
883
  }
696
884
 
697
885
  function extractReadyQueueSection(content) {
@@ -864,6 +1052,13 @@ const GIT_PRIVACY_COLLAPSE_ROOTS = [
864
1052
  'session-logs',
865
1053
  ];
866
1054
 
1055
+ const GIT_PRIVACY_AGENT_ROOTS = new Set([
1056
+ '.agy',
1057
+ '.claude',
1058
+ '.codex',
1059
+ '.gemini',
1060
+ ]);
1061
+
867
1062
  function scanPackagePrivacy(root) {
868
1063
  const packagePath = join(root, 'package.json');
869
1064
  if (!existsSync(packagePath)) return [];
@@ -895,7 +1090,7 @@ function scanPackagePrivacy(root) {
895
1090
  return issues;
896
1091
  }
897
1092
 
898
- function scanGitPrivacy(root) {
1093
+ function scanGitPrivacy(root, config) {
899
1094
  const gitDir = join(root, '.git');
900
1095
  if (!existsSync(gitDir)) return [];
901
1096
 
@@ -907,10 +1102,10 @@ function scanGitPrivacy(root) {
907
1102
  if (result.status !== 0) return [];
908
1103
 
909
1104
  const tracked = result.stdout.split('\n').filter(Boolean);
910
- return summarizeGitPrivacyIssues(tracked);
1105
+ return summarizeGitPrivacyIssues(tracked, config);
911
1106
  }
912
1107
 
913
- function summarizeGitPrivacyIssues(tracked) {
1108
+ function summarizeGitPrivacyIssues(tracked, config) {
914
1109
  const grouped = new Map();
915
1110
  const singles = [];
916
1111
 
@@ -925,6 +1120,7 @@ function summarizeGitPrivacyIssues(tracked) {
925
1120
  }
926
1121
 
927
1122
  const issues = [];
1123
+ const agentRootSummaries = [];
928
1124
 
929
1125
  for (const file of singles.sort()) {
930
1126
  issues.push({
@@ -935,6 +1131,10 @@ function summarizeGitPrivacyIssues(tracked) {
935
1131
 
936
1132
  for (const root of Array.from(grouped.keys()).sort()) {
937
1133
  const files = grouped.get(root).slice().sort(compareGitPrivacyFiles);
1134
+ if (GIT_PRIVACY_AGENT_ROOTS.has(root)) {
1135
+ agentRootSummaries.push(`${root}/ (${files.length} files)`);
1136
+ continue;
1137
+ }
938
1138
  const samples = files.slice(0, 5);
939
1139
  const hiddenCount = files.length - samples.length;
940
1140
  const noun = files.length === 1 ? 'path' : 'paths';
@@ -947,6 +1147,21 @@ function summarizeGitPrivacyIssues(tracked) {
947
1147
  issues.push({ issue, fix, details });
948
1148
  }
949
1149
 
1150
+ if (agentRootSummaries.length) {
1151
+ issues.unshift({
1152
+ issue: `Tracked shared agent trees detected: ${agentRootSummaries.join(', ')}`,
1153
+ fix: config.doctor.allowTrackedAgentTrees
1154
+ ? undefined
1155
+ : 'If these agent trees are intentionally versioned in this repo, keep them. Otherwise untrack them without deleting local files: `git rm --cached -r -- <path>`, then add an ignore rule.',
1156
+ details: [
1157
+ config.doctor.allowTrackedAgentTrees
1158
+ ? 'Allowed by `.nexus/config.json` because this repo intentionally versions shared agent trees.'
1159
+ : 'This can be normal in private repos that share agent protocols and memory in Git.',
1160
+ ],
1161
+ ok: config.doctor.allowTrackedAgentTrees,
1162
+ });
1163
+ }
1164
+
950
1165
  return issues;
951
1166
  }
952
1167
 
@@ -969,6 +1184,26 @@ function gitPrivacyPriority(file) {
969
1184
  return 2;
970
1185
  }
971
1186
 
1187
+ function createColors() {
1188
+ const enabled = supportsColor();
1189
+ const wrap = (open, close) => (value) => enabled ? `\u001b[${open}m${value}\u001b[${close}m` : String(value);
1190
+ return {
1191
+ bold: wrap(1, 22),
1192
+ dim: wrap(2, 22),
1193
+ red: wrap(31, 39),
1194
+ green: wrap(32, 39),
1195
+ yellow: wrap(33, 39),
1196
+ blue: wrap(34, 39),
1197
+ cyan: wrap(36, 39),
1198
+ };
1199
+ }
1200
+
1201
+ function supportsColor() {
1202
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
1203
+ if ('NO_COLOR' in process.env) return false;
1204
+ return Boolean(process.stdout && process.stdout.isTTY);
1205
+ }
1206
+
972
1207
  function scanGeneratedArtifacts(root) {
973
1208
  const gitDir = join(root, '.git');
974
1209
  if (!existsSync(gitDir)) return [];
package/src/lib/config.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Config — resolve project root and Nexus paths
3
3
  */
4
4
 
5
- import { existsSync } from 'fs';
5
+ import { existsSync, readFileSync } from 'fs';
6
6
  import { resolve, join } from 'path';
7
7
  import { cwd } from 'process';
8
8
 
@@ -14,6 +14,7 @@ export function getConfig(fromDir) {
14
14
  const root = fromDir || cwd();
15
15
  const lockDir = join(root, '.nexus', 'locks');
16
16
  const budgetFile = join(root, '.nexus', 'agent-budgets.json');
17
+ const localConfig = readLocalConfig(root);
17
18
 
18
19
  _config = {
19
20
  root,
@@ -28,6 +29,9 @@ export function getConfig(fromDir) {
28
29
  maxDumpFiles: 20,
29
30
  maxClaimAttempts: 10,
30
31
  claimRetryMs: 2000,
32
+ doctor: {
33
+ allowTrackedAgentTrees: Boolean(localConfig.doctor?.allowTrackedAgentTrees),
34
+ },
31
35
  };
32
36
 
33
37
  return _config;
@@ -36,3 +40,15 @@ export function getConfig(fromDir) {
36
40
  export function resetConfig() {
37
41
  _config = null;
38
42
  }
43
+
44
+ function readLocalConfig(root) {
45
+ const path = join(root, '.nexus', 'config.json');
46
+ if (!existsSync(path)) return {};
47
+
48
+ try {
49
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
50
+ return parsed && typeof parsed === 'object' ? parsed : {};
51
+ } catch {
52
+ return {};
53
+ }
54
+ }