@inkobytes/nexus 1.0.0 → 1.0.2
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 +11 -0
- package/README.md +29 -0
- package/bin/nexus.js +96 -51
- package/nexus-dashboard/docs/index.html +82 -99
- package/nexus-dashboard/index.html +210 -19
- package/nexus-dashboard/style.css +419 -38
- package/package.json +2 -1
- package/src/commands/completion.js +124 -0
- package/src/commands/dashboard.js +67 -0
- package/src/commands/doctor.js +81 -4
- package/src/commands/init.js +2 -0
- package/src/commands/status.js +5 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nexus completion zsh
|
|
3
|
+
* Print shell completion scripts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export default function completion(args) {
|
|
7
|
+
const shell = args[0] || 'zsh';
|
|
8
|
+
|
|
9
|
+
switch (shell) {
|
|
10
|
+
case 'zsh':
|
|
11
|
+
console.log(buildZshCompletion());
|
|
12
|
+
return;
|
|
13
|
+
case '--help':
|
|
14
|
+
case '-h':
|
|
15
|
+
case 'help':
|
|
16
|
+
printHelp();
|
|
17
|
+
return;
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unsupported completion shell: ${shell}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`Usage: nexus completion zsh
|
|
25
|
+
|
|
26
|
+
Print a shell completion script for Nexus.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
source <(nexus completion zsh)
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildZshCompletion() {
|
|
34
|
+
return `#compdef nexus
|
|
35
|
+
|
|
36
|
+
local -a commands
|
|
37
|
+
commands=(
|
|
38
|
+
'init:Scaffold Nexus files into current repo'
|
|
39
|
+
'doctor:Check or repair agent protocol files'
|
|
40
|
+
'completion:Print a shell completion script'
|
|
41
|
+
'checkin:Signal agent presence'
|
|
42
|
+
'checkout:Signal session end or cleanup'
|
|
43
|
+
'claim:Lock a file or directory'
|
|
44
|
+
'release:Unlock, auto-commit, and log'
|
|
45
|
+
'standup:Append a validated standup line'
|
|
46
|
+
'status:Show current blackboard state'
|
|
47
|
+
'clean:Prune locks'
|
|
48
|
+
'next:Suggest next safe task from queue'
|
|
49
|
+
'start:Orient an agent entering this repo'
|
|
50
|
+
'dashboard:Serve the local dashboard'
|
|
51
|
+
'metrics:Summarize commits, releases, and queue cost'
|
|
52
|
+
'ledger:Show or backfill completed task ledger'
|
|
53
|
+
'chmod:Show or set promptCHMOD permissions'
|
|
54
|
+
'db:Database backup and recovery'
|
|
55
|
+
'drill:Inspect or run protocol drills'
|
|
56
|
+
'soul:Manage local soul overlay in agent files'
|
|
57
|
+
'help:Show command help'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
local -a drill_actions db_actions
|
|
61
|
+
drill_actions=(list show run report help)
|
|
62
|
+
db_actions=(backup list restore schedule)
|
|
63
|
+
|
|
64
|
+
case $CURRENT in
|
|
65
|
+
2)
|
|
66
|
+
_describe 'nexus command' commands
|
|
67
|
+
return
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
case $words[2] in
|
|
72
|
+
checkin|checkout|next)
|
|
73
|
+
_arguments '1:agent:(@agy @claude @codex @gemini)'
|
|
74
|
+
;;
|
|
75
|
+
claim)
|
|
76
|
+
_arguments \\
|
|
77
|
+
'1:path:_files' \\
|
|
78
|
+
'2:agent:(@agy @claude @codex @gemini)' \\
|
|
79
|
+
'3:intent:_message' \\
|
|
80
|
+
'--agent[Agent handle]:agent:(@agy @claude @codex @gemini)' \\
|
|
81
|
+
'--intent[Claim intent]:intent:_message'
|
|
82
|
+
;;
|
|
83
|
+
release)
|
|
84
|
+
_arguments '1:path:_files' '2:commit message:_message'
|
|
85
|
+
;;
|
|
86
|
+
standup)
|
|
87
|
+
_arguments '1:standup message:_message'
|
|
88
|
+
;;
|
|
89
|
+
doctor)
|
|
90
|
+
_arguments '--fix[Repair known protocol drift]' '--json[Print JSON report]'
|
|
91
|
+
;;
|
|
92
|
+
completion)
|
|
93
|
+
_arguments '1:shell:(zsh)'
|
|
94
|
+
;;
|
|
95
|
+
clean)
|
|
96
|
+
_arguments '1:target or flag:(--stale)'
|
|
97
|
+
;;
|
|
98
|
+
dashboard)
|
|
99
|
+
_arguments '--serve[Start the dashboard server]' '--port[Dashboard port]:port number:'
|
|
100
|
+
;;
|
|
101
|
+
metrics)
|
|
102
|
+
_arguments '--json[Print JSON]'
|
|
103
|
+
;;
|
|
104
|
+
ledger)
|
|
105
|
+
_arguments '1:mode:(backfill --json)'
|
|
106
|
+
;;
|
|
107
|
+
chmod)
|
|
108
|
+
_arguments '--list[List current permissions]' '--init[Initialize promptCHMOD permissions]'
|
|
109
|
+
;;
|
|
110
|
+
db)
|
|
111
|
+
_arguments '1:db action:(\${db_actions[*]})'
|
|
112
|
+
;;
|
|
113
|
+
drill)
|
|
114
|
+
_arguments '1:drill action:(\${drill_actions[*]})'
|
|
115
|
+
;;
|
|
116
|
+
soul)
|
|
117
|
+
_arguments '--file[Overlay file path]:path:_files' '--status[Show status]' '--remove[Remove overlay]'
|
|
118
|
+
;;
|
|
119
|
+
start)
|
|
120
|
+
_arguments '--agent[Agent handle]:agent:(@agy @claude @codex @gemini)'
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
@@ -54,6 +54,7 @@ export function buildSnapshot() {
|
|
|
54
54
|
const queueText = readText(config.queue);
|
|
55
55
|
const standupText = readText(config.standup);
|
|
56
56
|
const reportText = readText(config.report);
|
|
57
|
+
const parsedReport = parseReportBlocks(reportText);
|
|
57
58
|
const git = getGitStatus(config.root);
|
|
58
59
|
|
|
59
60
|
return {
|
|
@@ -69,6 +70,8 @@ export function buildSnapshot() {
|
|
|
69
70
|
ledger: readLedgerEntries().reverse(),
|
|
70
71
|
standup: parseStandupEntries(standupText).filter(entry => entry.type.startsWith('@')).slice(-8).reverse(),
|
|
71
72
|
releases: parseReleaseEntries(reportText).slice(-16).reverse(),
|
|
73
|
+
reportIntro: parsedReport.intro,
|
|
74
|
+
reportBlocks: parsedReport.blocks,
|
|
72
75
|
report: sortReportBlocksLatestFirst(reportText),
|
|
73
76
|
};
|
|
74
77
|
}
|
|
@@ -364,6 +367,70 @@ function hasReleaseDetailValue(detail) {
|
|
|
364
367
|
return detail.slice(colon + 1).trim().length > 0;
|
|
365
368
|
}
|
|
366
369
|
|
|
370
|
+
function parseReportBlocks(content) {
|
|
371
|
+
const trimmed = content.trim();
|
|
372
|
+
if (!trimmed) return { intro: '', blocks: [] };
|
|
373
|
+
|
|
374
|
+
const firstBlock = trimmed.search(/^## \[/m);
|
|
375
|
+
if (firstBlock === -1) return { intro: trimmed, blocks: [] };
|
|
376
|
+
|
|
377
|
+
const intro = trimmed.slice(0, firstBlock).trimEnd();
|
|
378
|
+
const blocks = trimmed
|
|
379
|
+
.slice(firstBlock)
|
|
380
|
+
.split(/\n(?=## \[)/)
|
|
381
|
+
.map((block) => block.trimEnd())
|
|
382
|
+
.filter(Boolean)
|
|
383
|
+
.reverse()
|
|
384
|
+
.map((block, index) => {
|
|
385
|
+
const lines = block.split('\n');
|
|
386
|
+
const heading = lines[0]?.match(/^## \[([^\]]+)\]\s*(.*)$/);
|
|
387
|
+
const timestamp = heading?.[1]?.trim() || '';
|
|
388
|
+
const target = heading?.[2]?.trim() || lines[0]?.replace(/^##\s*/, '') || '';
|
|
389
|
+
const details = lines.slice(1).join('\n').trim();
|
|
390
|
+
const parsed = parseReportTimestamp(timestamp);
|
|
391
|
+
const monthKey = parsed
|
|
392
|
+
? `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}`
|
|
393
|
+
: '';
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
id: `report-${index}`,
|
|
397
|
+
timestamp,
|
|
398
|
+
target,
|
|
399
|
+
details,
|
|
400
|
+
raw: block,
|
|
401
|
+
monthKey,
|
|
402
|
+
monthLabel: parsed ? formatReportMonth(parsed) : 'Undated',
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return { intro, blocks };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseReportTimestamp(value) {
|
|
410
|
+
const match = String(value || '').match(
|
|
411
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s?(AM|PM|am|pm))?)?$/
|
|
412
|
+
);
|
|
413
|
+
if (!match) return null;
|
|
414
|
+
|
|
415
|
+
const year = Number.parseInt(match[1], 10);
|
|
416
|
+
const month = Number.parseInt(match[2], 10) - 1;
|
|
417
|
+
const day = Number.parseInt(match[3], 10);
|
|
418
|
+
let hour = Number.parseInt(match[4] || '0', 10);
|
|
419
|
+
const minute = Number.parseInt(match[5] || '0', 10);
|
|
420
|
+
const second = Number.parseInt(match[6] || '0', 10);
|
|
421
|
+
const meridiem = (match[7] || '').toUpperCase();
|
|
422
|
+
|
|
423
|
+
if (meridiem === 'PM' && hour < 12) hour += 12;
|
|
424
|
+
if (meridiem === 'AM' && hour === 12) hour = 0;
|
|
425
|
+
|
|
426
|
+
const date = new Date(year, month, day, hour, minute, second);
|
|
427
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function formatReportMonth(date) {
|
|
431
|
+
return date.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
|
432
|
+
}
|
|
433
|
+
|
|
367
434
|
function sortReportBlocksLatestFirst(content) {
|
|
368
435
|
const trimmed = content.trim();
|
|
369
436
|
if (!trimmed) return content;
|
package/src/commands/doctor.js
CHANGED
|
@@ -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.
|
|
@@ -673,6 +675,11 @@ export default function doctor(args) {
|
|
|
673
675
|
for (const entry of entries) {
|
|
674
676
|
const prefix = entry.ok ? '-' : '!';
|
|
675
677
|
console.log(` ${prefix} ${entry.issue}`);
|
|
678
|
+
if (entry.details) {
|
|
679
|
+
for (const detail of entry.details) {
|
|
680
|
+
console.log(` ${detail}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
676
683
|
if (entry.fix) console.log(` Fix: ${entry.fix}`);
|
|
677
684
|
if (!entry.ok) problemCount++;
|
|
678
685
|
}
|
|
@@ -843,6 +850,20 @@ const PRIVATE_GIT_PATHS = [
|
|
|
843
850
|
'USER.md',
|
|
844
851
|
];
|
|
845
852
|
|
|
853
|
+
const GIT_PRIVACY_COLLAPSE_ROOTS = [
|
|
854
|
+
'.agent-session-logs',
|
|
855
|
+
'.agent-*',
|
|
856
|
+
'.agy',
|
|
857
|
+
'.antigravitycli',
|
|
858
|
+
'.claude',
|
|
859
|
+
'.codex',
|
|
860
|
+
'.gemini',
|
|
861
|
+
'.nexus/local',
|
|
862
|
+
'docs-priv',
|
|
863
|
+
'scratch',
|
|
864
|
+
'session-logs',
|
|
865
|
+
];
|
|
866
|
+
|
|
846
867
|
function scanPackagePrivacy(root) {
|
|
847
868
|
const packagePath = join(root, 'package.json');
|
|
848
869
|
if (!existsSync(packagePath)) return [];
|
|
@@ -886,10 +907,66 @@ function scanGitPrivacy(root) {
|
|
|
886
907
|
if (result.status !== 0) return [];
|
|
887
908
|
|
|
888
909
|
const tracked = result.stdout.split('\n').filter(Boolean);
|
|
889
|
-
return tracked
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
910
|
+
return summarizeGitPrivacyIssues(tracked);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function summarizeGitPrivacyIssues(tracked) {
|
|
914
|
+
const grouped = new Map();
|
|
915
|
+
const singles = [];
|
|
916
|
+
|
|
917
|
+
for (const file of tracked) {
|
|
918
|
+
const root = gitPrivacyRoot(file);
|
|
919
|
+
if (!root) {
|
|
920
|
+
singles.push(file);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (!grouped.has(root)) grouped.set(root, []);
|
|
924
|
+
grouped.get(root).push(file);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const issues = [];
|
|
928
|
+
|
|
929
|
+
for (const file of singles.sort()) {
|
|
930
|
+
issues.push({
|
|
931
|
+
issue: `Git tracks private/local path: ${file}`,
|
|
932
|
+
fix: 'Untrack it without deleting local files: `git rm --cached -r -- <path>`, then add an ignore rule.',
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
for (const root of Array.from(grouped.keys()).sort()) {
|
|
937
|
+
const files = grouped.get(root).slice().sort(compareGitPrivacyFiles);
|
|
938
|
+
const samples = files.slice(0, 5);
|
|
939
|
+
const hiddenCount = files.length - samples.length;
|
|
940
|
+
const noun = files.length === 1 ? 'path' : 'paths';
|
|
941
|
+
const issue = `Git tracks private/local ${noun} under ${root}/ (${files.length} files)`;
|
|
942
|
+
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.';
|
|
943
|
+
const details = samples.map((file) => `sample: ${file}`);
|
|
944
|
+
if (hiddenCount > 0) {
|
|
945
|
+
details.push(`...and ${hiddenCount} more tracked paths`);
|
|
946
|
+
}
|
|
947
|
+
issues.push({ issue, fix, details });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return issues;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function gitPrivacyRoot(file) {
|
|
954
|
+
for (const root of GIT_PRIVACY_COLLAPSE_ROOTS) {
|
|
955
|
+
if (matchesPrivatePath(file, root)) {
|
|
956
|
+
return root.replace(/\/$/, '');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return '';
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function compareGitPrivacyFiles(a, b) {
|
|
963
|
+
return gitPrivacyPriority(a) - gitPrivacyPriority(b) || a.localeCompare(b);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function gitPrivacyPriority(file) {
|
|
967
|
+
if (/\/(CONTINUITY\.md|memories\/)/.test(file)) return 0;
|
|
968
|
+
if (/\/(AGENTS\.md|CLAUDE\.md|GEMINI\.md)$/.test(file)) return 1;
|
|
969
|
+
return 2;
|
|
893
970
|
}
|
|
894
971
|
|
|
895
972
|
function scanGeneratedArtifacts(root) {
|
package/src/commands/init.js
CHANGED
|
@@ -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.
|
package/src/commands/status.js
CHANGED
|
@@ -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('');
|