@inkobytes/nexus 1.0.1 → 1.0.3

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,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.3 - 2026-06-03
4
+
5
+ - Collapsed large `nexus doctor` Git Privacy floods into grouped per-root summaries with sample paths.
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
+
3
11
  ## 1.0.1 - 2026-06-02
4
12
 
5
13
  - Added colorized `nexus help` output for a more readable CLI experience.
package/README.md CHANGED
@@ -166,6 +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, with shared agent dirs collapsed into one concise note
170
+ - colorized action buckets so fixes and informational lock notes are easier to scan
169
171
  - stale nexus locks
170
172
  - missing agent instructions specifically for nexus
171
173
  - missing continuity and memory scaffolds
@@ -175,6 +177,18 @@ With `--fix`, Nexus creates safe missing scaffolds and updates managed protocol
175
177
 
176
178
  With `--json`, Nexus prints the same health sections as structured JSON for tools such as Inkobytes reports.
177
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
+
178
192
  Use `doctor` for audit or repair. Do not make it the normal first command for every agent session.
179
193
 
180
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.1",
3
+ "version": "1.0.3",
4
4
  "description": "Multi-agent coordination CLI for coding agents sharing a local repository",
5
5
  "type": "module",
6
6
  "bin": {
@@ -93,6 +93,8 @@ This project uses Nexus for multi-agent coordination.
93
93
  ### Nexus Rules
94
94
 
95
95
  - Claim before editing shared project files: \`nexus claim <path> @Agent "intent"\`.
96
+ - Nexus is agent-native and file-native, not feature-native: commit and release by file as soon as the file reaches a coherent checkpoint.
97
+ - Do not hold claims while waiting to bundle related files into one feature commit; that blocks other agents who may need the file next.
96
98
  - Release finished work through Nexus: \`nexus release <path> "commit message"\`.
97
99
  - Use \`nexus next @Agent\` for the next safe queue task.
98
100
  - Do not free-roam into unassigned or \`Auto-flow: no\` work without user approval.
@@ -345,6 +347,7 @@ export default function doctor(args) {
345
347
  const fix = args.includes('--fix');
346
348
  const json = args.includes('--json');
347
349
  const root = cwd();
350
+ const colors = createColors();
348
351
  const sections = {
349
352
  'Nexus Files': [],
350
353
  'Agent Instructions': [],
@@ -363,8 +366,8 @@ export default function doctor(args) {
363
366
  const config = getConfig(root);
364
367
 
365
368
  if (!json) {
366
- console.log(`Nexus doctor${fix ? ' --fix' : ''}`);
367
- console.log(`Repo: ${root}\n`);
369
+ console.log(colors.bold(colors.cyan(`Nexus doctor${fix ? ' --fix' : ''}`)));
370
+ console.log(`${colors.dim('Repo:')} ${root}\n`);
368
371
  }
369
372
 
370
373
  const nexusProtocolFiles = ['_NEXUS_CONSTITUTION.md', '_NEXUS_QUEUE.md', '_NEXUS_STANDUP.md'];
@@ -410,7 +413,7 @@ export default function doctor(args) {
410
413
  sections['Package Privacy'].push(issue);
411
414
  }
412
415
 
413
- for (const issue of scanGitPrivacy(root)) {
416
+ for (const issue of scanGitPrivacy(root, config)) {
414
417
  sections['Git Privacy'].push(issue);
415
418
  }
416
419
 
@@ -656,35 +659,95 @@ export default function doctor(args) {
656
659
  }
657
660
 
658
661
  if (changes.length) {
659
- console.log('Applied fixes:');
660
- for (const change of changes) console.log(` - ${change}`);
662
+ console.log(colors.bold(colors.green('Applied fixes')));
663
+ for (const change of changes) console.log(` ${colors.green('-')} ${change}`);
661
664
  console.log('');
662
665
  }
663
666
 
664
667
  let problemCount = 0;
665
668
  for (const [title, entries] of Object.entries(sections)) {
666
- console.log(`[${title}]`);
669
+ console.log(colors.bold(colors.cyan(`[${title}]`)));
667
670
  if (!entries.length) {
668
- console.log(' OK');
671
+ console.log(` ${colors.green('OK')}`);
669
672
  console.log('');
670
673
  continue;
671
674
  }
672
675
 
673
- for (const entry of entries) {
674
- const prefix = entry.ok ? '-' : '!';
675
- console.log(` ${prefix} ${entry.issue}`);
676
- if (entry.fix) console.log(` Fix: ${entry.fix}`);
677
- if (!entry.ok) problemCount++;
678
- }
676
+ const actionable = entries.filter((entry) => !entry.ok);
677
+ const informational = entries.filter((entry) => entry.ok);
678
+ problemCount += actionable.length;
679
+
680
+ if (actionable.length) renderEntryBucket('Fix the following', actionable, colors.yellow, colors.red, title, colors);
681
+ if (informational.length) renderEntryBucket('Review / informational', informational, colors.blue, colors.green, title, colors);
679
682
  console.log('');
680
683
  }
681
684
 
682
685
  if (problemCount) {
683
- console.log('Some issues need attention. Safe scaffold fixes: `nexus doctor --fix`.');
686
+ console.log(colors.bold(colors.yellow('Some issues need attention.')));
687
+ console.log(colors.dim('Safe scaffold fixes: `nexus doctor --fix`.'));
684
688
  return;
685
689
  }
686
690
 
687
- console.log('All checked Nexus categories are ready.');
691
+ console.log(colors.bold(colors.green('All checked Nexus categories are ready.')));
692
+ }
693
+
694
+ function renderEntryBucket(label, entries, headingColor, markerColor, sectionTitle, colors) {
695
+ console.log(` ${headingColor(label)}`);
696
+ const groups = groupEntriesForDisplay(entries, sectionTitle);
697
+ for (const group of groups) {
698
+ if (group.label) {
699
+ console.log(` ${colors.bold(group.label)}`);
700
+ }
701
+ for (const entry of group.entries) {
702
+ const baseIndent = group.label ? ' ' : ' ';
703
+ const detailIndent = group.label ? ' ' : ' ';
704
+ const prefix = entry.ok ? '-' : '!';
705
+ console.log(`${baseIndent}${markerColor(prefix)} ${entry.issue}`);
706
+ if (entry.details) {
707
+ for (const detail of entry.details) {
708
+ console.log(`${detailIndent}${colors.dim(detail)}`);
709
+ }
710
+ }
711
+ if (entry.fix) {
712
+ console.log(`${detailIndent}${colors.bold('Fix:')} ${colors.dim(entry.fix)}`);
713
+ }
714
+ }
715
+ }
716
+ }
717
+
718
+ function groupEntriesForDisplay(entries, sectionTitle) {
719
+ const groups = [];
720
+ const grouped = new Map();
721
+
722
+ for (const entry of entries) {
723
+ const label = entry.displayGroup || inferDisplayGroup(entry.issue, sectionTitle);
724
+ if (!label) {
725
+ groups.push({ label: '', entries: [entry] });
726
+ continue;
727
+ }
728
+ if (!grouped.has(label)) {
729
+ const group = { label, entries: [] };
730
+ grouped.set(label, group);
731
+ groups.push(group);
732
+ }
733
+ grouped.get(label).entries.push(entry);
734
+ }
735
+
736
+ return groups;
737
+ }
738
+
739
+ function inferDisplayGroup(issue, sectionTitle) {
740
+ if (sectionTitle === 'Locks') {
741
+ const lockMatch = issue.match(/^(?:Stale lock on|Active lock on|Unverified claim on) ([^ ]+)/);
742
+ if (lockMatch) return lockMatch[1];
743
+ }
744
+
745
+ if (sectionTitle === 'Queue Authorship') {
746
+ const taskMatch = issue.match(/^Task "([^"]+)"/);
747
+ if (taskMatch) return taskMatch[1];
748
+ }
749
+
750
+ return '';
688
751
  }
689
752
 
690
753
  function extractReadyQueueSection(content) {
@@ -843,6 +906,27 @@ const PRIVATE_GIT_PATHS = [
843
906
  'USER.md',
844
907
  ];
845
908
 
909
+ const GIT_PRIVACY_COLLAPSE_ROOTS = [
910
+ '.agent-session-logs',
911
+ '.agent-*',
912
+ '.agy',
913
+ '.antigravitycli',
914
+ '.claude',
915
+ '.codex',
916
+ '.gemini',
917
+ '.nexus/local',
918
+ 'docs-priv',
919
+ 'scratch',
920
+ 'session-logs',
921
+ ];
922
+
923
+ const GIT_PRIVACY_AGENT_ROOTS = new Set([
924
+ '.agy',
925
+ '.claude',
926
+ '.codex',
927
+ '.gemini',
928
+ ]);
929
+
846
930
  function scanPackagePrivacy(root) {
847
931
  const packagePath = join(root, 'package.json');
848
932
  if (!existsSync(packagePath)) return [];
@@ -874,7 +958,7 @@ function scanPackagePrivacy(root) {
874
958
  return issues;
875
959
  }
876
960
 
877
- function scanGitPrivacy(root) {
961
+ function scanGitPrivacy(root, config) {
878
962
  const gitDir = join(root, '.git');
879
963
  if (!existsSync(gitDir)) return [];
880
964
 
@@ -886,10 +970,106 @@ function scanGitPrivacy(root) {
886
970
  if (result.status !== 0) return [];
887
971
 
888
972
  const tracked = result.stdout.split('\n').filter(Boolean);
889
- return tracked.map((file) => ({
890
- issue: `Git tracks private/local path: ${file}`,
891
- fix: 'Untrack it without deleting local files: `git rm --cached -r -- <path>`, then add an ignore rule.',
892
- }));
973
+ return summarizeGitPrivacyIssues(tracked, config);
974
+ }
975
+
976
+ function summarizeGitPrivacyIssues(tracked, config) {
977
+ const grouped = new Map();
978
+ const singles = [];
979
+
980
+ for (const file of tracked) {
981
+ const root = gitPrivacyRoot(file);
982
+ if (!root) {
983
+ singles.push(file);
984
+ continue;
985
+ }
986
+ if (!grouped.has(root)) grouped.set(root, []);
987
+ grouped.get(root).push(file);
988
+ }
989
+
990
+ const issues = [];
991
+ const agentRootSummaries = [];
992
+
993
+ for (const file of singles.sort()) {
994
+ issues.push({
995
+ issue: `Git tracks private/local path: ${file}`,
996
+ fix: 'Untrack it without deleting local files: `git rm --cached -r -- <path>`, then add an ignore rule.',
997
+ });
998
+ }
999
+
1000
+ for (const root of Array.from(grouped.keys()).sort()) {
1001
+ const files = grouped.get(root).slice().sort(compareGitPrivacyFiles);
1002
+ if (GIT_PRIVACY_AGENT_ROOTS.has(root)) {
1003
+ agentRootSummaries.push(`${root}/ (${files.length} files)`);
1004
+ continue;
1005
+ }
1006
+ const samples = files.slice(0, 5);
1007
+ const hiddenCount = files.length - samples.length;
1008
+ const noun = files.length === 1 ? 'path' : 'paths';
1009
+ const issue = `Git tracks private/local ${noun} under ${root}/ (${files.length} files)`;
1010
+ const fix = 'Review the sample paths below. If the tree is intentionally tracked in this repo, keep it. Otherwise untrack it without deleting local files: `git rm --cached -r -- <path>`, then add an ignore rule.';
1011
+ const details = samples.map((file) => `sample: ${file}`);
1012
+ if (hiddenCount > 0) {
1013
+ details.push(`...and ${hiddenCount} more tracked paths`);
1014
+ }
1015
+ issues.push({ issue, fix, details });
1016
+ }
1017
+
1018
+ if (agentRootSummaries.length) {
1019
+ issues.unshift({
1020
+ issue: `Tracked shared agent trees detected: ${agentRootSummaries.join(', ')}`,
1021
+ fix: config.doctor.allowTrackedAgentTrees
1022
+ ? undefined
1023
+ : '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.',
1024
+ details: [
1025
+ config.doctor.allowTrackedAgentTrees
1026
+ ? 'Allowed by `.nexus/config.json` because this repo intentionally versions shared agent trees.'
1027
+ : 'This can be normal in private repos that share agent protocols and memory in Git.',
1028
+ ],
1029
+ ok: config.doctor.allowTrackedAgentTrees,
1030
+ });
1031
+ }
1032
+
1033
+ return issues;
1034
+ }
1035
+
1036
+ function gitPrivacyRoot(file) {
1037
+ for (const root of GIT_PRIVACY_COLLAPSE_ROOTS) {
1038
+ if (matchesPrivatePath(file, root)) {
1039
+ return root.replace(/\/$/, '');
1040
+ }
1041
+ }
1042
+ return '';
1043
+ }
1044
+
1045
+ function compareGitPrivacyFiles(a, b) {
1046
+ return gitPrivacyPriority(a) - gitPrivacyPriority(b) || a.localeCompare(b);
1047
+ }
1048
+
1049
+ function gitPrivacyPriority(file) {
1050
+ if (/\/(CONTINUITY\.md|memories\/)/.test(file)) return 0;
1051
+ if (/\/(AGENTS\.md|CLAUDE\.md|GEMINI\.md)$/.test(file)) return 1;
1052
+ return 2;
1053
+ }
1054
+
1055
+ function createColors() {
1056
+ const enabled = supportsColor();
1057
+ const wrap = (open, close) => (value) => enabled ? `\u001b[${open}m${value}\u001b[${close}m` : String(value);
1058
+ return {
1059
+ bold: wrap(1, 22),
1060
+ dim: wrap(2, 22),
1061
+ red: wrap(31, 39),
1062
+ green: wrap(32, 39),
1063
+ yellow: wrap(33, 39),
1064
+ blue: wrap(34, 39),
1065
+ cyan: wrap(36, 39),
1066
+ };
1067
+ }
1068
+
1069
+ function supportsColor() {
1070
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
1071
+ if ('NO_COLOR' in process.env) return false;
1072
+ return Boolean(process.stdout && process.stdout.isTTY);
893
1073
  }
894
1074
 
895
1075
  function scanGeneratedArtifacts(root) {
@@ -379,6 +379,8 @@ This project uses Nexus for multi-agent coordination.
379
379
  ### Nexus Rules
380
380
 
381
381
  - Claim before editing shared project files: \`nexus claim <path> @Agent "intent"\`.
382
+ - Nexus is agent-native and file-native, not feature-native: commit and release by file as soon as the file reaches a coherent checkpoint.
383
+ - Do not hold claims while waiting to bundle related files into one feature commit; that blocks other agents who may need the file next.
382
384
  - Release finished work through Nexus: \`nexus release <path> "commit message"\`.
383
385
  - Use \`nexus next @Agent\` for the next safe queue task.
384
386
  - Do not free-roam into unassigned or \`Auto-flow: no\` work without user approval.
@@ -7,6 +7,8 @@ import { listLocks } from '../lib/lockManager.js';
7
7
  import { getConfig } from '../lib/config.js';
8
8
  import { spawnSync } from 'child_process';
9
9
 
10
+ const FILE_FLOW_WARNING = 'Stale claim. Commit this file now.';
11
+
10
12
  export default function status(args) {
11
13
  const config = getConfig();
12
14
  const locks = listLocks();
@@ -36,6 +38,9 @@ export default function status(args) {
36
38
  const staleTag = stale ? ' ⚠️ STALE' : '';
37
39
  console.log(` 🔒 ${lock.target}`);
38
40
  console.log(` Agent: ${agent} | Age: ${ageStr}${staleTag}`);
41
+ if (stale) {
42
+ console.log(` Warning: ${FILE_FLOW_WARNING}`);
43
+ }
39
44
  }
40
45
 
41
46
  console.log('');
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
+ }