@inkobytes/nexus 1.0.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +455 -0
  3. package/bin/nexus.js +108 -0
  4. package/drills/nexus-agent-protocol/README.md +65 -0
  5. package/drills/nexus-agent-protocol/cases/blocked.yaml +20 -0
  6. package/drills/nexus-agent-protocol/cases/claim-before-edit.yaml +16 -0
  7. package/drills/nexus-agent-protocol/cases/current-file-state.yaml +15 -0
  8. package/drills/nexus-agent-protocol/cases/data-boundary-table-header.yaml +21 -0
  9. package/drills/nexus-agent-protocol/cases/data-mutation-delete-rows.yaml +20 -0
  10. package/drills/nexus-agent-protocol/cases/done-claim-adversarial.yaml +18 -0
  11. package/drills/nexus-agent-protocol/cases/ghost-file-claim-loop.yaml +16 -0
  12. package/drills/nexus-agent-protocol/cases/issue-found.yaml +21 -0
  13. package/drills/nexus-agent-protocol/cases/private-path-protection.yaml +23 -0
  14. package/drills/nexus-agent-protocol/cases/queue-is-thin-index.yaml +21 -0
  15. package/drills/nexus-agent-protocol/cases/removal-scope.yaml +26 -0
  16. package/drills/nexus-agent-protocol/cases/remove-agent-folders-from-git.yaml +24 -0
  17. package/drills/nexus-agent-protocol/cases/stale-lock-after-commit.yaml +26 -0
  18. package/drills/nexus-agent-protocol/cases/start-does-not-replace-claim-release.yaml +17 -0
  19. package/drills/nexus-agent-protocol/cases/task-contract.yaml +23 -0
  20. package/drills/nexus-agent-protocol/cases/vendor-cleanup-preserve-history.yaml +24 -0
  21. package/drills/nexus-agent-protocol/cases/wrong-repo-push.yaml +23 -0
  22. package/nexus-dashboard/docs/index.html +183 -0
  23. package/nexus-dashboard/index.html +678 -0
  24. package/nexus-dashboard/logo-nexus.svg +14 -0
  25. package/nexus-dashboard/style.css +1454 -0
  26. package/package.json +42 -0
  27. package/skills/nexus/SKILL.md +62 -0
  28. package/src/commands/checkin.js +19 -0
  29. package/src/commands/checkout.js +33 -0
  30. package/src/commands/chmod.js +93 -0
  31. package/src/commands/claim.js +122 -0
  32. package/src/commands/clean.js +76 -0
  33. package/src/commands/dashboard.js +387 -0
  34. package/src/commands/db.js +256 -0
  35. package/src/commands/doctor.js +958 -0
  36. package/src/commands/drill.js +507 -0
  37. package/src/commands/help.js +8 -0
  38. package/src/commands/init.js +576 -0
  39. package/src/commands/ledger.js +215 -0
  40. package/src/commands/metrics.js +178 -0
  41. package/src/commands/next.js +317 -0
  42. package/src/commands/release.js +107 -0
  43. package/src/commands/soul.js +156 -0
  44. package/src/commands/standup.js +59 -0
  45. package/src/commands/start.js +126 -0
  46. package/src/commands/status.js +109 -0
  47. package/src/hooks/pre-migration-backup.js +35 -0
  48. package/src/lib/agentScopes.js +61 -0
  49. package/src/lib/blackboard.js +90 -0
  50. package/src/lib/config.js +38 -0
  51. package/src/lib/dump.js +63 -0
  52. package/src/lib/git.js +111 -0
  53. package/src/lib/lockManager.js +302 -0
  54. package/src/lib/pathSafety.js +41 -0
  55. package/src/lib/permissions.js +74 -0
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@inkobytes/nexus",
3
+ "version": "1.0.0",
4
+ "description": "Multi-agent coordination CLI for coding agents sharing a local repository",
5
+ "type": "module",
6
+ "bin": {
7
+ "nexus": "bin/nexus.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test test/**/*.test.js"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "agents",
15
+ "multi-agent",
16
+ "coordination",
17
+ "swarm",
18
+ "cli",
19
+ "claude",
20
+ "codex",
21
+ "gemini",
22
+ "git"
23
+ ],
24
+ "author": "Carmelyne Thompson <carmelyne@inkobytes.com>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/carmelyne/inkobytes-nexus.git"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "files": [
34
+ "bin/",
35
+ "src/",
36
+ "drills/",
37
+ "skills/",
38
+ "nexus-dashboard/",
39
+ "LICENSE",
40
+ "README.md"
41
+ ]
42
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: nexus
3
+ description: Use in repos coordinated by the Nexus CLI, especially when _NEXUS_CONSTITUTION.md, _NEXUS_QUEUE.md, _NEXUS_STANDUP.md, or .nexus/ exists, or when the user asks about Nexus start/claim/release/doctor/status/next.
4
+ ---
5
+
6
+ # Nexus
7
+
8
+ Nexus CLI is the coordination engine. This skill is only the agent playbook.
9
+
10
+ ## Loop
11
+
12
+ 1. Run `nexus start`; set `NEXUS_AGENT` for your CLI, or pass `--agent @agy|@claude|@codex|@gemini`. Start is orientation only, not permission to edit.
13
+ 2. Read `USER.md` if present for local user preferences.
14
+ 3. Read continuity and latest memory when present.
15
+ 4. Read `_NEXUS_QUEUE.md` and `_NEXUS_STANDUP.md`.
16
+ 5. Choose user-assigned work or `nexus next @Agent`; do not free-roam into `Auto-flow: no`.
17
+ 6. Claim exact shared files before reading/editing:
18
+
19
+ ```bash
20
+ nexus claim <path> @Agent "intent"
21
+ ```
22
+
23
+ 7. Treat claim output as current file state. Ignore cached file memory when contents matter.
24
+ 8. Work only inside the claimed surface and run focused validation.
25
+ 9. If the user wants a commit, release through Nexus:
26
+
27
+ ```bash
28
+ nexus release <path> "short commit message"
29
+ ```
30
+
31
+ ## Queue Items
32
+
33
+ When adding work to `_NEXUS_QUEUE.md`, keep tasks dashboard-parseable and immediately actionable. Use this shape:
34
+
35
+ ```md
36
+ - [ ] TASK/@agent: Short task title
37
+ - Id: stable-kebab-id
38
+ - Epic: Product area or safety theme
39
+ - Status: Ready
40
+ - Depends on: none
41
+ - Files: path/one.js, path/two.md
42
+ - Affinity: cli, docs, dashboard
43
+ - Cost: small
44
+ - Auto-flow: yes
45
+ - Notes: One practical paragraph with scope, constraints, and definition of done.
46
+ ```
47
+
48
+ - `Files` should name the likely edit surface so other agents can spot conflicts before claiming.
49
+ - `Depends on` should list hard blockers by `Id`; use `none` when the task is independent.
50
+ - `Auto-flow: yes` means an agent can grab it after `nexus next`; use `no` when planning or human approval is needed.
51
+ - `Notes` should carry dashboard-useful context, not a whole design doc.
52
+
53
+ ## Guardrails
54
+
55
+ - Ask before `nexus doctor --fix` unless scaffold repair is already approved.
56
+ - Use `nexus doctor` for audit/repair, not as the normal startup command.
57
+ - Use CLI/model names as lock handles: `@agy`, `@claude`, `@codex`, `@gemini`.
58
+ - Agent-local continuity and memory files are claim-exempt unless the user says otherwise.
59
+ - When using subagents or parallel workers, the lead agent owns the repo effects: claim the full path scope, pass boundaries down, re-read affected files, and mention delegated work in release or standup notes.
60
+ - Avoid parallel `nexus release`.
61
+ - Do not install packages younger than 14 days; if age is unknown, ask.
62
+ - Use `nexus status`, `nexus clean --stale`, or surgical `nexus clean <path>` for lock recovery. Never nuke all locks without explicit approval.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * nexus checkin @agent — signal agent presence
3
+ */
4
+
5
+ import { writeFileSync, mkdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { env, cwd } from 'process';
8
+
9
+ export default function checkin(args = []) {
10
+ const root = cwd();
11
+ const agent = args[0] || env.NEXUS_AGENT || '@unknown';
12
+ const presenceDir = join(root, '.nexus', 'presence');
13
+ const presenceFile = join(presenceDir, agent.replace(/^@/, ''));
14
+
15
+ mkdirSync(presenceDir, { recursive: true });
16
+ writeFileSync(presenceFile, String(Math.floor(Date.now() / 1000)), 'utf-8');
17
+
18
+ console.log(`[CHECKIN] ${agent} is active.`);
19
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * nexus checkout @agent — signal agent session end
3
+ */
4
+
5
+ import { unlinkSync, existsSync, readdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { env, cwd } from 'process';
8
+
9
+ export default function checkout(args = []) {
10
+ const root = cwd();
11
+ const presenceDir = join(root, '.nexus', 'presence');
12
+ const all = args.includes('--all');
13
+
14
+ if (all) {
15
+ if (existsSync(presenceDir)) {
16
+ for (const file of readdirSync(presenceDir)) {
17
+ unlinkSync(join(presenceDir, file));
18
+ }
19
+ console.log(`[CHECKOUT] All agent presence cleared.`);
20
+ }
21
+ return;
22
+ }
23
+
24
+ const agent = args[0] || env.NEXUS_AGENT || '@unknown';
25
+ const presenceFile = join(presenceDir, agent.replace(/^@/, ''));
26
+
27
+ if (existsSync(presenceFile)) {
28
+ unlinkSync(presenceFile);
29
+ console.log(`[CHECKOUT] ${agent} is offline.`);
30
+ } else {
31
+ console.log(`[CHECKOUT] No presence record for ${agent}.`);
32
+ }
33
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * nexus chmod — manage the promptCHMOD permission matrix.
3
+ * Human-controlled only: blocks unverified sessions from editing.
4
+ *
5
+ * r = read for reference
6
+ * w = modify (claim/release already enforces this)
7
+ * x = treat as authoritative instructions (the injection surface)
8
+ */
9
+
10
+ import { DEFAULT_MATRIX, loadPermissions, getChmodPath } from '../lib/permissions.js';
11
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
12
+
13
+ const VALID_PERMS = /^[r-][w-][x-]$/;
14
+
15
+ export default function chmod(args) {
16
+ const chmodPath = getChmodPath();
17
+
18
+ if (args.includes('--init')) {
19
+ if (existsSync(chmodPath)) {
20
+ console.log('[INFO] _NEXUS_CHMOD.md already exists. Use --list to inspect or edit directly.');
21
+ return;
22
+ }
23
+ writeFileSync(chmodPath, DEFAULT_MATRIX, 'utf-8');
24
+ console.log('[OK] Created _NEXUS_CHMOD.md with default permission matrix.');
25
+ return;
26
+ }
27
+
28
+ if (args.length === 0 || args.includes('--list')) {
29
+ if (!existsSync(chmodPath)) {
30
+ console.log('[WARN] No _NEXUS_CHMOD.md found. Run `nexus chmod --init` to create defaults.');
31
+ return;
32
+ }
33
+ const entries = loadPermissions();
34
+ if (!entries.length) {
35
+ console.log('No permissions defined.');
36
+ return;
37
+ }
38
+ console.log('\npromptCHMOD — permission matrix\n');
39
+ console.log(' ' + 'PATH'.padEnd(36) + 'PERMS AGENT NOTE');
40
+ console.log(' ' + '─'.repeat(72));
41
+ for (const e of entries) {
42
+ const note = e.perms[2] === 'x' ? 'authoritative' : 'reference only';
43
+ console.log(' ' + e.path.padEnd(36) + e.perms + ' ' + e.agent.padEnd(14) + note);
44
+ }
45
+ console.log('');
46
+ return;
47
+ }
48
+
49
+ // nexus chmod <path> <perms> [agent]
50
+ const positional = args.filter(a => !a.startsWith('--'));
51
+ const [target, perms, agent = 'all'] = positional;
52
+
53
+ if (!target || !perms) {
54
+ console.error('Usage: nexus chmod <path> <perms> [agent]\n e.g. nexus chmod USER.md r-x all\nFlags: --list, --init');
55
+ process.exit(1);
56
+ }
57
+ if (!VALID_PERMS.test(perms)) {
58
+ console.error('[ERROR] perms must be 3 chars: [r-][w-][x-] e.g. r--, rw-, r-x, rwx');
59
+ process.exit(1);
60
+ }
61
+
62
+ // Human-only gate — ties into agent-identity trust detection
63
+ const trustSource = process.env.CLAUDECODE === '1' ? 'harness'
64
+ : process.env.NEXUS_AGENT ? 'operator'
65
+ : 'unverified';
66
+ if (trustSource === 'unverified') {
67
+ console.error('[ERROR] nexus chmod requires a verified session (CLAUDECODE=1 or NEXUS_AGENT set).\nThe permission matrix is human-controlled — agents cannot self-elevate.');
68
+ process.exit(1);
69
+ }
70
+
71
+ const content = existsSync(chmodPath) ? readFileSync(chmodPath, 'utf-8') : DEFAULT_MATRIX;
72
+ const lines = content.split('\n');
73
+ const agentNorm = agent.toLowerCase();
74
+ const targetNorm = target.replace(/^\.\//, '');
75
+
76
+ let found = false;
77
+ const newLines = lines.map(line => {
78
+ const trimmed = line.trim();
79
+ if (!trimmed || trimmed.startsWith('#')) return line;
80
+ const parts = trimmed.split(/\s+/);
81
+ if (parts[0] === targetNorm && (parts[2] || 'all').toLowerCase() === agentNorm) {
82
+ found = true;
83
+ return targetNorm.padEnd(36) + perms + ' ' + agentNorm;
84
+ }
85
+ return line;
86
+ });
87
+
88
+ if (!found) newLines.push(targetNorm.padEnd(36) + perms + ' ' + agentNorm);
89
+ writeFileSync(chmodPath, newLines.join('\n'), 'utf-8');
90
+
91
+ const note = perms[2] === 'x' ? 'authoritative (x-on)' : 'reference only (x-off)';
92
+ console.log(`[OK] ${targetNorm} → ${perms} for ${agentNorm} — ${note}`);
93
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * nexus claim <path> <agent> "<intent>"
3
+ * Lock a file or directory, update blackboard, dump fresh state.
4
+ */
5
+
6
+ import { appendEntry } from '../lib/blackboard.js';
7
+ import { acquireLock } from '../lib/lockManager.js';
8
+ import { dumpState } from '../lib/dump.js';
9
+ import { existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { cwd } from 'process';
12
+ import { normalizeTarget } from '../lib/pathSafety.js';
13
+ import { CANONICAL_MODEL_HANDLE_SET, CANONICAL_MODEL_HANDLES_TEXT, hasAgentAlias } from '../lib/agentScopes.js';
14
+
15
+ const CORE_FILES = [
16
+ '_NEXUS_CONSTITUTION.md',
17
+ '_NEXUS_QUEUE.md',
18
+ '_NEXUS_STANDUP.md',
19
+ ];
20
+ const THINKING_LEVELS = new Set(['low', 'medium', 'high']);
21
+
22
+ function missingCoreFiles() {
23
+ const root = cwd();
24
+ return CORE_FILES.filter((file) => !existsSync(join(root, file)));
25
+ }
26
+
27
+ function shouldWarnAgentHandle(agent) {
28
+ const normalized = agent.toLowerCase();
29
+ return normalized === '@agent'
30
+ || normalized === 'unknownagent'
31
+ || (normalized.startsWith('@') && !CANONICAL_MODEL_HANDLE_SET.has(normalized) && hasAgentAlias(normalized));
32
+ }
33
+
34
+ function isAgentHandle(agent) {
35
+ return typeof agent === 'string' && /^@[a-z0-9][a-z0-9_-]*$/i.test(agent);
36
+ }
37
+
38
+ function readFlag(args, name) {
39
+ const index = args.indexOf(name);
40
+ if (index === -1) return '';
41
+
42
+ const value = args[index + 1];
43
+ const hasValue = value && !String(value).startsWith('--');
44
+ args.splice(index, hasValue ? 2 : 1);
45
+ return hasValue ? value : '';
46
+ }
47
+
48
+ export default function claim(args) {
49
+ const positional = [...args];
50
+
51
+ const agentFlag = readFlag(positional, '--agent').trim();
52
+ const intentFlag = readFlag(positional, '--intent').trim();
53
+ const subagents = parseInt(readFlag(positional, '--subagents'), 10) || 0;
54
+ const model = readFlag(positional, '--model').trim();
55
+ const thinking = readFlag(positional, '--thinking').trim().toLowerCase();
56
+
57
+ let target = positional[0];
58
+ const agent = agentFlag || positional[1] || '';
59
+ const intent = intentFlag || positional[2] || '';
60
+ const hasAgent = Boolean(agentFlag || positional[1]);
61
+ const hasIntent = Boolean(intentFlag || positional[2]);
62
+
63
+ if (!target) {
64
+ console.error('Usage: nexus claim <filepath_or_dir> <agent> "<intent>" [--agent <agent>] [--intent <intent>] [--subagents <n>] [--model <name>] [--thinking <low|medium|high>]');
65
+ process.exit(1);
66
+ }
67
+
68
+ if (!hasAgent || !isAgentHandle(agent)) {
69
+ console.error('[ERROR] Missing or invalid claim agent.');
70
+ console.error('Use: nexus claim <path> @codex "intent"');
71
+ console.error('Or: nexus claim <path> --agent @codex --intent "intent"');
72
+ process.exit(1);
73
+ }
74
+
75
+ if (!hasIntent) {
76
+ console.error('[ERROR] Missing claim intent.');
77
+ console.error('Use: nexus claim <path> @codex "intent"');
78
+ console.error('Or: nexus claim <path> --agent @codex --intent "intent"');
79
+ process.exit(1);
80
+ }
81
+
82
+ if (thinking && !THINKING_LEVELS.has(thinking)) {
83
+ console.error('[ERROR] --thinking must be one of: low, medium, high');
84
+ process.exit(1);
85
+ }
86
+
87
+ try {
88
+ target = normalizeTarget(target);
89
+ } catch (err) {
90
+ console.error(`[ERROR] ${err.message}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ const result = acquireLock(target, agent, intent, subagents, { model, thinking });
95
+
96
+ if (!result.success) {
97
+ console.error(`[ERROR] ${result.message}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ const missing = missingCoreFiles();
102
+ if (missing.length) {
103
+ console.warn(`[WARN] Missing Nexus protocol files: ${missing.join(', ')}. Run \`nexus doctor\`.`);
104
+ }
105
+
106
+ if (!CANONICAL_MODEL_HANDLE_SET.has(agent.toLowerCase()) && shouldWarnAgentHandle(agent)) {
107
+ console.warn(`[WARN] Use CLI/model names as lock handles: ${CANONICAL_MODEL_HANDLES_TEXT}.`);
108
+ }
109
+
110
+ if (!model) {
111
+ console.warn('[WARN] Claim has no model metadata. Add `--model <name>` when available.');
112
+ }
113
+
114
+ // Update blackboard
115
+ appendEntry(`- 🔒 **${target}** - Locked by **${agent}**: ${intent}`);
116
+
117
+ console.log(result.message);
118
+
119
+ // Dump fresh file state
120
+ const state = dumpState(target);
121
+ console.log(state);
122
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * nexus clean [--stale | <path>]
3
+ * Surgical, stale, or nuke lock cleanup.
4
+ */
5
+
6
+ import { removeEntry, clearBoard } from '../lib/blackboard.js';
7
+ import { releaseLock, breakStaleLocks, nukeAllLocks, getLockPath } from '../lib/lockManager.js';
8
+ import { existsSync } from 'fs';
9
+ import { createInterface } from 'readline';
10
+ import { normalizeTarget } from '../lib/pathSafety.js';
11
+
12
+ export default async function clean(args) {
13
+ const mode = args[0];
14
+
15
+ // Mode: stale — prune expired locks
16
+ if (mode === '--stale') {
17
+ const broken = breakStaleLocks();
18
+
19
+ if (broken.length === 0) {
20
+ console.log('No stale locks found.');
21
+ return;
22
+ }
23
+
24
+ for (const { target, age } of broken) {
25
+ removeEntry(`🔒 **${target}**`);
26
+ console.log(`🩺 [STALE CLEAN] Unlocked: ${target} (${age}s old)`);
27
+ }
28
+ return;
29
+ }
30
+
31
+ // Mode: surgical — clean specific file/dir
32
+ if (mode && mode !== '--nuke') {
33
+ let target;
34
+ try {
35
+ target = normalizeTarget(mode);
36
+ } catch (err) {
37
+ console.error(`[ERROR] ${err.message}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ const lockPath = getLockPath(target);
42
+
43
+ if (existsSync(lockPath)) {
44
+ releaseLock(target);
45
+ removeEntry(`🔒 **${target}**`);
46
+ console.log(`🩺 [SURGICAL CLEAN] Unlocked: ${target}`);
47
+ } else {
48
+ console.log(`No lock found for: ${target}`);
49
+ }
50
+ return;
51
+ }
52
+
53
+ // Mode: nuke — clear everything (with confirmation)
54
+ const confirmed = await confirm('⚠️ NUKE ALL LOCKS? (y/N): ');
55
+
56
+ if (confirmed) {
57
+ nukeAllLocks();
58
+ clearBoard();
59
+ console.log('[SYSTEM] Cleared all locks.');
60
+ } else {
61
+ console.log('Cancelled.');
62
+ }
63
+ }
64
+
65
+ function confirm(prompt) {
66
+ return new Promise((resolve) => {
67
+ const rl = createInterface({
68
+ input: process.stdin,
69
+ output: process.stdout,
70
+ });
71
+ rl.question(prompt, (answer) => {
72
+ rl.close();
73
+ resolve(answer.toLowerCase() === 'y');
74
+ });
75
+ });
76
+ }