@inkobytes/nexus 1.0.2 → 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 +4 -1
- package/README.md +14 -1
- package/package.json +1 -1
- package/src/commands/doctor.js +123 -20
- package/src/lib/config.js +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.0.
|
|
3
|
+
## 1.0.3 - 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.
|
|
7
10
|
|
|
8
11
|
## 1.0.1 - 2026-06-02
|
|
9
12
|
|
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
|
|
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
package/src/commands/doctor.js
CHANGED
|
@@ -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(
|
|
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
|
|
|
@@ -658,40 +659,95 @@ export default function doctor(args) {
|
|
|
658
659
|
}
|
|
659
660
|
|
|
660
661
|
if (changes.length) {
|
|
661
|
-
console.log('Applied fixes
|
|
662
|
-
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}`);
|
|
663
664
|
console.log('');
|
|
664
665
|
}
|
|
665
666
|
|
|
666
667
|
let problemCount = 0;
|
|
667
668
|
for (const [title, entries] of Object.entries(sections)) {
|
|
668
|
-
console.log(`[${title}]`);
|
|
669
|
+
console.log(colors.bold(colors.cyan(`[${title}]`)));
|
|
669
670
|
if (!entries.length) {
|
|
670
|
-
console.log('
|
|
671
|
+
console.log(` ${colors.green('OK')}`);
|
|
671
672
|
console.log('');
|
|
672
673
|
continue;
|
|
673
674
|
}
|
|
674
675
|
|
|
675
|
-
|
|
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);
|
|
682
|
+
console.log('');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (problemCount) {
|
|
686
|
+
console.log(colors.bold(colors.yellow('Some issues need attention.')));
|
|
687
|
+
console.log(colors.dim('Safe scaffold fixes: `nexus doctor --fix`.'));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
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 ? ' ' : ' ';
|
|
676
704
|
const prefix = entry.ok ? '-' : '!';
|
|
677
|
-
console.log(
|
|
705
|
+
console.log(`${baseIndent}${markerColor(prefix)} ${entry.issue}`);
|
|
678
706
|
if (entry.details) {
|
|
679
707
|
for (const detail of entry.details) {
|
|
680
|
-
console.log(
|
|
708
|
+
console.log(`${detailIndent}${colors.dim(detail)}`);
|
|
681
709
|
}
|
|
682
710
|
}
|
|
683
|
-
if (entry.fix)
|
|
684
|
-
|
|
711
|
+
if (entry.fix) {
|
|
712
|
+
console.log(`${detailIndent}${colors.bold('Fix:')} ${colors.dim(entry.fix)}`);
|
|
713
|
+
}
|
|
685
714
|
}
|
|
686
|
-
console.log('');
|
|
687
715
|
}
|
|
716
|
+
}
|
|
688
717
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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];
|
|
692
743
|
}
|
|
693
744
|
|
|
694
|
-
|
|
745
|
+
if (sectionTitle === 'Queue Authorship') {
|
|
746
|
+
const taskMatch = issue.match(/^Task "([^"]+)"/);
|
|
747
|
+
if (taskMatch) return taskMatch[1];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return '';
|
|
695
751
|
}
|
|
696
752
|
|
|
697
753
|
function extractReadyQueueSection(content) {
|
|
@@ -864,6 +920,13 @@ const GIT_PRIVACY_COLLAPSE_ROOTS = [
|
|
|
864
920
|
'session-logs',
|
|
865
921
|
];
|
|
866
922
|
|
|
923
|
+
const GIT_PRIVACY_AGENT_ROOTS = new Set([
|
|
924
|
+
'.agy',
|
|
925
|
+
'.claude',
|
|
926
|
+
'.codex',
|
|
927
|
+
'.gemini',
|
|
928
|
+
]);
|
|
929
|
+
|
|
867
930
|
function scanPackagePrivacy(root) {
|
|
868
931
|
const packagePath = join(root, 'package.json');
|
|
869
932
|
if (!existsSync(packagePath)) return [];
|
|
@@ -895,7 +958,7 @@ function scanPackagePrivacy(root) {
|
|
|
895
958
|
return issues;
|
|
896
959
|
}
|
|
897
960
|
|
|
898
|
-
function scanGitPrivacy(root) {
|
|
961
|
+
function scanGitPrivacy(root, config) {
|
|
899
962
|
const gitDir = join(root, '.git');
|
|
900
963
|
if (!existsSync(gitDir)) return [];
|
|
901
964
|
|
|
@@ -907,10 +970,10 @@ function scanGitPrivacy(root) {
|
|
|
907
970
|
if (result.status !== 0) return [];
|
|
908
971
|
|
|
909
972
|
const tracked = result.stdout.split('\n').filter(Boolean);
|
|
910
|
-
return summarizeGitPrivacyIssues(tracked);
|
|
973
|
+
return summarizeGitPrivacyIssues(tracked, config);
|
|
911
974
|
}
|
|
912
975
|
|
|
913
|
-
function summarizeGitPrivacyIssues(tracked) {
|
|
976
|
+
function summarizeGitPrivacyIssues(tracked, config) {
|
|
914
977
|
const grouped = new Map();
|
|
915
978
|
const singles = [];
|
|
916
979
|
|
|
@@ -925,6 +988,7 @@ function summarizeGitPrivacyIssues(tracked) {
|
|
|
925
988
|
}
|
|
926
989
|
|
|
927
990
|
const issues = [];
|
|
991
|
+
const agentRootSummaries = [];
|
|
928
992
|
|
|
929
993
|
for (const file of singles.sort()) {
|
|
930
994
|
issues.push({
|
|
@@ -935,6 +999,10 @@ function summarizeGitPrivacyIssues(tracked) {
|
|
|
935
999
|
|
|
936
1000
|
for (const root of Array.from(grouped.keys()).sort()) {
|
|
937
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
|
+
}
|
|
938
1006
|
const samples = files.slice(0, 5);
|
|
939
1007
|
const hiddenCount = files.length - samples.length;
|
|
940
1008
|
const noun = files.length === 1 ? 'path' : 'paths';
|
|
@@ -947,6 +1015,21 @@ function summarizeGitPrivacyIssues(tracked) {
|
|
|
947
1015
|
issues.push({ issue, fix, details });
|
|
948
1016
|
}
|
|
949
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
|
+
|
|
950
1033
|
return issues;
|
|
951
1034
|
}
|
|
952
1035
|
|
|
@@ -969,6 +1052,26 @@ function gitPrivacyPriority(file) {
|
|
|
969
1052
|
return 2;
|
|
970
1053
|
}
|
|
971
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);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
972
1075
|
function scanGeneratedArtifacts(root) {
|
|
973
1076
|
const gitDir = join(root, '.git');
|
|
974
1077
|
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
|
+
}
|