@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
@@ -0,0 +1,107 @@
1
+ /**
2
+ * nexus release <path> "<commit message>"
3
+ * Unlock, auto-commit, update blackboard, log to report.
4
+ */
5
+
6
+ import { appendFileSync } from 'fs';
7
+ import { removeEntry } from '../lib/blackboard.js';
8
+ import { listLocks, readGitHead, releaseLock } from '../lib/lockManager.js';
9
+ import { stageAndCommit } from '../lib/git.js';
10
+ import { getConfig } from '../lib/config.js';
11
+ import { normalizeTarget } from '../lib/pathSafety.js';
12
+ import { appendCompletedLedgerEntries } from './ledger.js';
13
+
14
+ export default function release(args) {
15
+ let target = args[0];
16
+
17
+ if (!target) {
18
+ console.error('Usage: nexus release <filepath_or_dir> "<commit message>"');
19
+ process.exit(1);
20
+ }
21
+
22
+ try {
23
+ target = normalizeTarget(target);
24
+ } catch (err) {
25
+ console.error(`[ERROR] ${err.message}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ const commitMsg = args[1] || `chore: agent updated ${target}`;
30
+ const lock = listLocks().find((entry) => entry.target === target);
31
+ const config = getConfig();
32
+ const releaseHead = readGitHead(config.root);
33
+ const claimHead = lock?.claimHead || 'unknown';
34
+ const hasHeadDrift = claimHead !== 'unknown' && releaseHead !== 'unknown' && claimHead !== releaseHead;
35
+
36
+ if (hasHeadDrift) {
37
+ console.warn(`[WARN] HEAD changed since claim for ${target}: claimed ${shortSha(claimHead)}, releasing from ${shortSha(releaseHead)}. Review interleaved commits if needed.`);
38
+ }
39
+
40
+ // Stage and commit first
41
+ const gitResult = stageAndCommit(target, commitMsg, lock?.agent || '');
42
+ if (!gitResult.success && !gitResult.message?.includes('clean')) {
43
+ console.error(`[ERROR] ${gitResult.message}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ // Release the lock
48
+ const lockResult = releaseLock(target);
49
+ if (!lockResult.success) {
50
+ console.warn(`[WARN] ${lockResult.message}`);
51
+ }
52
+
53
+ // Remove from blackboard
54
+ removeEntry(`🔒 **${target}**`);
55
+
56
+ if (target === '_NEXUS_REPORT.md') {
57
+ console.log('[INFO] Skipped report append for _NEXUS_REPORT.md release to avoid self-noise.');
58
+ console.log('[LOCK RELEASED & COMMITTED]');
59
+ return;
60
+ }
61
+
62
+ // Append to report
63
+ const timestamp = formatReportTimestamp(new Date());
64
+ const reportLine = `## [${timestamp}] ${target}
65
+
66
+ - Agent: ${lock?.agent || 'unknown'}
67
+ - Target: ${target}
68
+ - Claim HEAD: ${claimHead}
69
+ - Release HEAD: ${releaseHead}
70
+ - Drift: ${hasHeadDrift ? 'yes' : 'no'}
71
+ - SHA: ${gitResult.sha || 'unknown'}
72
+ - Commit: ${commitMsg}
73
+
74
+ `;
75
+
76
+ try {
77
+ appendFileSync(config.report, reportLine, 'utf-8');
78
+ } catch { /* report file might not exist yet */ }
79
+
80
+ try {
81
+ const count = appendCompletedLedgerEntries({
82
+ target,
83
+ agent: lock?.agent || 'unknown',
84
+ sha: gitResult.sha || 'unknown',
85
+ commit: commitMsg,
86
+ });
87
+ if (count) console.log(`[LEDGER] Added ${count} completed task entr${count === 1 ? 'y' : 'ies'}.`);
88
+ } catch { /* ledger is reporting only; release should still complete */ }
89
+
90
+ console.log('[LOCK RELEASED & COMMITTED]');
91
+ }
92
+
93
+ function shortSha(sha) {
94
+ return sha === 'unknown' ? sha : sha.slice(0, 7);
95
+ }
96
+
97
+ function formatReportTimestamp(date) {
98
+ const yyyy = date.getFullYear();
99
+ const mm = String(date.getMonth() + 1).padStart(2, '0');
100
+ const dd = String(date.getDate()).padStart(2, '0');
101
+ const rawHour = date.getHours();
102
+ const hour = rawHour % 12 || 12;
103
+ const minute = String(date.getMinutes()).padStart(2, '0');
104
+ const second = String(date.getSeconds()).padStart(2, '0');
105
+ const period = rawHour < 12 ? 'AM' : 'PM';
106
+ return `${yyyy}-${mm}-${dd} ${String(hour).padStart(2, '0')}:${minute}:${second} ${period}`;
107
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * nexus soul - apply a local soul overlay to agent instruction files
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
6
+ import { dirname, join } from 'path';
7
+ import { cwd } from 'process';
8
+
9
+ const DEFAULT_OVERLAY_PATH = '.nexus/local/agent-overlay.md';
10
+ const START_MARKER_PREFIX = '<!-- NEXUS-LOCAL-SOUL:START';
11
+ const END_MARKER = '<!-- NEXUS-LOCAL-SOUL:END -->';
12
+
13
+ const AGENT_ENTRYPOINTS = [
14
+ '.agy/AGENTS.md',
15
+ '.codex/AGENTS.md',
16
+ '.claude/CLAUDE.md',
17
+ '.gemini/GEMINI.md',
18
+ ];
19
+
20
+ const DEFAULT_OVERLAY = `# Local Soul Overlay
21
+
22
+ Add your local agent behavior notes here. This file is local workspace flavor:
23
+ tone, response rhythm, naming defaults, and other private context that should
24
+ sit outside the public Nexus protocol block.
25
+ `;
26
+
27
+ function parseArgs(args) {
28
+ const fileIndex = args.indexOf('--file');
29
+ const mode = args.includes('--status') ? 'status' : args.includes('--remove') ? 'remove' : 'apply';
30
+ if (fileIndex === -1) return { overlayPath: DEFAULT_OVERLAY_PATH, mode };
31
+ if (!args[fileIndex + 1]) {
32
+ throw new Error('Usage: nexus soul [--file <path>] [--status | --remove]');
33
+ }
34
+ return { overlayPath: args[fileIndex + 1], mode };
35
+ }
36
+
37
+ function localSoulBlock(overlayPath, content) {
38
+ return `${START_MARKER_PREFIX} ${overlayPath} -->
39
+ ${content.trim()}
40
+ ${END_MARKER}`;
41
+ }
42
+
43
+ function upsertSoulBlock(existing, overlayPath, overlayContent) {
44
+ const start = existing.indexOf(START_MARKER_PREFIX);
45
+ const end = existing.indexOf(END_MARKER);
46
+ const block = localSoulBlock(overlayPath, overlayContent);
47
+
48
+ if (start !== -1 && end !== -1 && end > start) {
49
+ const before = existing.slice(0, start).trimEnd();
50
+ const after = existing.slice(end + END_MARKER.length).trimStart();
51
+ return `${before}\n\n${block}\n${after ? `\n${after}` : ''}`;
52
+ }
53
+
54
+ const firstHeadingEnd = existing.indexOf('\n');
55
+ if (firstHeadingEnd !== -1 && existing.startsWith('# ')) {
56
+ const title = existing.slice(0, firstHeadingEnd).trimEnd();
57
+ const body = existing.slice(firstHeadingEnd).trimStart();
58
+ return `${title}\n\n${block}\n\n${body}`;
59
+ }
60
+
61
+ return `${block}\n\n${existing.trimStart()}`;
62
+ }
63
+
64
+ function removeSoulBlock(existing) {
65
+ const start = existing.indexOf(START_MARKER_PREFIX);
66
+ const end = existing.indexOf(END_MARKER);
67
+ if (start === -1 || end === -1 || end <= start) return existing;
68
+
69
+ const before = existing.slice(0, start).trimEnd();
70
+ const after = existing.slice(end + END_MARKER.length).trimStart();
71
+ return `${before}${before && after ? '\n\n' : ''}${after ? `${after}\n` : ''}`;
72
+ }
73
+
74
+ function hasSoulBlock(existing) {
75
+ const start = existing.indexOf(START_MARKER_PREFIX);
76
+ const end = existing.indexOf(END_MARKER);
77
+ return start !== -1 && end !== -1 && end > start;
78
+ }
79
+
80
+ export default function soul(args) {
81
+ const root = cwd();
82
+ const { overlayPath, mode } = parseArgs(args);
83
+ const fullOverlayPath = join(root, overlayPath);
84
+
85
+ if (mode === 'status') {
86
+ console.log('Nexus soul status');
87
+ console.log(`Overlay: ${overlayPath} (${existsSync(fullOverlayPath) ? 'exists' : 'missing'})`);
88
+
89
+ for (const entrypoint of AGENT_ENTRYPOINTS) {
90
+ const path = join(root, entrypoint);
91
+ const state = existsSync(path) && hasSoulBlock(readFileSync(path, 'utf-8')) ? 'applied' : 'missing';
92
+ console.log(` - ${entrypoint}: ${state}`);
93
+ }
94
+ return;
95
+ }
96
+
97
+ if (mode === 'remove') {
98
+ const removed = [];
99
+ for (const entrypoint of AGENT_ENTRYPOINTS) {
100
+ const path = join(root, entrypoint);
101
+ if (!existsSync(path)) continue;
102
+ const existing = readFileSync(path, 'utf-8');
103
+ const next = removeSoulBlock(existing);
104
+ if (next === existing) continue;
105
+ writeFileSync(path, next, 'utf-8');
106
+ removed.push(entrypoint);
107
+ }
108
+
109
+ if (removed.length) {
110
+ console.log('Removed local soul overlay:');
111
+ for (const entrypoint of removed) console.log(` - ${entrypoint}`);
112
+ } else {
113
+ console.log('No local soul overlay blocks found.');
114
+ }
115
+ return;
116
+ }
117
+
118
+ mkdirSync(dirname(fullOverlayPath), { recursive: true });
119
+
120
+ if (!existsSync(fullOverlayPath)) {
121
+ writeFileSync(fullOverlayPath, DEFAULT_OVERLAY, 'utf-8');
122
+ console.log(`Created ${overlayPath}`);
123
+ }
124
+
125
+ const overlayContent = readFileSync(fullOverlayPath, 'utf-8');
126
+ const updated = [];
127
+ const missing = [];
128
+
129
+ for (const entrypoint of AGENT_ENTRYPOINTS) {
130
+ const path = join(root, entrypoint);
131
+ if (!existsSync(path)) {
132
+ missing.push(entrypoint);
133
+ continue;
134
+ }
135
+
136
+ const existing = readFileSync(path, 'utf-8');
137
+ const next = upsertSoulBlock(existing, overlayPath, overlayContent);
138
+ if (next === existing) continue;
139
+
140
+ writeFileSync(path, next, 'utf-8');
141
+ updated.push(entrypoint);
142
+ }
143
+
144
+ if (updated.length) {
145
+ console.log('Applied local soul overlay:');
146
+ for (const entrypoint of updated) console.log(` - ${entrypoint}`);
147
+ } else {
148
+ console.log('Local soul overlay already up to date.');
149
+ }
150
+
151
+ if (missing.length) {
152
+ console.log('\nSkipped missing agent files:');
153
+ for (const entrypoint of missing) console.log(` - ${entrypoint}`);
154
+ console.log('Run `nexus init` first to scaffold agent files.');
155
+ }
156
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * nexus standup "YYYY-MM-DD HH:MM AM/PM @agent [STATUS]: message"
3
+ * Append a validated one-line standup message.
4
+ */
5
+
6
+ import { appendFileSync, existsSync, writeFileSync } from 'fs';
7
+ import { getConfig } from '../lib/config.js';
8
+
9
+ const STANDUP_FORMAT = 'YYYY-MM-DD HH:MM AM/PM @agent [STATUS]: message';
10
+ const STANDUP_LINE_RE = /^(\d{4})-(\d{2})-(\d{2}) (0[1-9]|1[0-2]):([0-5]\d) (AM|PM) (@[a-z0-9][a-z0-9_-]*) \[([A-Z][A-Z0-9_-]*)\]: (.+)$/;
11
+
12
+ export function validateStandupLine(line) {
13
+ const text = typeof line === 'string' ? line.trim() : '';
14
+
15
+ if (!text) {
16
+ return { ok: false, error: 'Missing standup message.' };
17
+ }
18
+
19
+ const match = text.match(STANDUP_LINE_RE);
20
+ if (!match) {
21
+ if (!/@[a-z0-9][a-z0-9_-]*/i.test(text)) {
22
+ return { ok: false, error: 'Missing or invalid standup agent.' };
23
+ }
24
+ return { ok: false, error: 'Invalid standup message format.' };
25
+ }
26
+
27
+ const [, yearText, monthText, dayText] = match;
28
+ if (!isRealDate(Number(yearText), Number(monthText), Number(dayText))) {
29
+ return { ok: false, error: 'Invalid standup date.' };
30
+ }
31
+
32
+ return { ok: true, line: text };
33
+ }
34
+
35
+ export default function standup(args = []) {
36
+ const line = args.join(' ').trim();
37
+ const result = validateStandupLine(line);
38
+
39
+ if (!result.ok) {
40
+ console.error(`[ERROR] ${result.error}`);
41
+ console.error(`Use: nexus standup "${STANDUP_FORMAT}"`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const config = getConfig();
46
+ if (!existsSync(config.standup)) {
47
+ writeFileSync(config.standup, '# Nexus Standup\n\n', 'utf-8');
48
+ }
49
+
50
+ appendFileSync(config.standup, `${result.line}\n`, 'utf-8');
51
+ console.log('[STANDUP] Message recorded.');
52
+ }
53
+
54
+ function isRealDate(year, month, day) {
55
+ const date = new Date(Date.UTC(year, month - 1, day));
56
+ return date.getUTCFullYear() === year
57
+ && date.getUTCMonth() === month - 1
58
+ && date.getUTCDate() === day;
59
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * nexus start - orient an agent entering a repo
3
+ */
4
+
5
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { cwd, env } from 'process';
8
+ import { spawnSync } from 'child_process';
9
+ import { listLocks } from '../lib/lockManager.js';
10
+ import { AGENT_SCOPE_ENTRIES, AGENT_SCOPES, normalizeAgentHandle } from '../lib/agentScopes.js';
11
+ import { loadPermissions, getChmodPath } from '../lib/permissions.js';
12
+
13
+ function git(args) {
14
+ const result = spawnSync('git', args, { cwd: cwd(), encoding: 'utf-8', stdio: 'pipe' });
15
+ return result.status === 0 ? result.stdout.trim() : '';
16
+ }
17
+
18
+ function readFirstLines(path, maxLines = 8) {
19
+ if (!existsSync(path)) return null;
20
+ return readFileSync(path, 'utf-8').split('\n').slice(0, maxLines).join('\n').trim();
21
+ }
22
+
23
+ function optionValue(args, name) {
24
+ const index = args.indexOf(name);
25
+ return index === -1 ? null : args[index + 1] || null;
26
+ }
27
+
28
+ function latestMemoryEntry(root, scopeDir) {
29
+ const memoryDir = join(root, scopeDir, 'memories');
30
+ if (!existsSync(memoryDir)) return null;
31
+
32
+ const entries = [];
33
+ for (const month of readdirSync(memoryDir)) {
34
+ const monthPath = join(memoryDir, month);
35
+ try {
36
+ if (!statSync(monthPath).isDirectory()) continue;
37
+ } catch {
38
+ continue;
39
+ }
40
+
41
+ for (const file of readdirSync(monthPath)) {
42
+ if (!file.endsWith('.md')) continue;
43
+ const path = join(monthPath, file);
44
+ try {
45
+ entries.push({ path: `${scopeDir}/memories/${month}/${file}`, mtimeMs: statSync(path).mtimeMs });
46
+ } catch { /* ignore unreadable entries */ }
47
+ }
48
+ }
49
+
50
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
51
+ return entries[0]?.path || null;
52
+ }
53
+
54
+ export default function start(args = []) {
55
+ const root = cwd();
56
+ const agent = normalizeAgentHandle(optionValue(args, '--agent') || args[0] || env.NEXUS_AGENT);
57
+ const scope = agent ? AGENT_SCOPES[agent] : null;
58
+ const branch = git(['branch', '--show-current']) || '(unknown)';
59
+ const commits = git(['log', '--oneline', '-n', '3']).split('\n').filter(Boolean);
60
+ const dirtyFiles = git(['status', '--porcelain']).split('\n').filter(Boolean);
61
+ const locks = listLocks();
62
+ const continuityPath = scope ? `${scope.dir}/CONTINUITY.md` : null;
63
+ const memoryIndexPath = scope ? `${scope.dir}/memories/INDEX.md` : null;
64
+ const continuity = scope ? readFirstLines(join(root, continuityPath)) : null;
65
+ const memoryEntry = scope ? latestMemoryEntry(root, scope.dir) : null;
66
+
67
+ console.log('Nexus start');
68
+ console.log(`Repo: ${root}`);
69
+ console.log(`Branch: ${branch}`);
70
+ console.log(`Agent: ${scope ? `${scope.label} (${agent})` : 'unspecified'}`);
71
+
72
+ console.log('\nLast commits:');
73
+ if (commits.length) {
74
+ for (const commit of commits) console.log(` - ${commit}`);
75
+ } else {
76
+ console.log(' none');
77
+ }
78
+
79
+ console.log(`\nDirty files: ${dirtyFiles.length}`);
80
+ for (const file of dirtyFiles.slice(0, 20)) console.log(` - ${file}`);
81
+ if (dirtyFiles.length > 20) console.log(` ... ${dirtyFiles.length - 20} more`);
82
+
83
+ console.log(`\nLocks: ${locks.length}`);
84
+ for (const lock of locks) {
85
+ const age = lock.age === null ? 'unknown age' : `${lock.age}s old`;
86
+ console.log(` - ${lock.target} (${age})`);
87
+ }
88
+
89
+ if (scope) {
90
+ console.log(`\nContinuity (${continuityPath}):`);
91
+ console.log(continuity ? continuity.split('\n').map((line) => ` ${line}`).join('\n') : ' missing');
92
+
93
+ console.log(`\nLatest local memory (${memoryIndexPath}):`);
94
+ console.log(` ${memoryEntry || 'none'}`);
95
+ } else {
96
+ console.log('\nAgent memory scopes:');
97
+ for (const [handle, item] of AGENT_SCOPE_ENTRIES) {
98
+ console.log(` - ${handle}: ${item.dir}/CONTINUITY.md, ${item.dir}/memories/INDEX.md`);
99
+ }
100
+ console.log(' Pick your model scope; do not read another model memory unless the user asks.');
101
+ }
102
+
103
+ // promptCHMOD — surface reference-only files so agents know what not to execute
104
+ if (existsSync(getChmodPath())) {
105
+ const perms = loadPermissions();
106
+ const xOff = perms.filter(e => e.perms[2] !== 'x' && (e.agent === 'all' || e.agent === agent));
107
+ const xOn = perms.filter(e => e.perms[2] === 'x' && (e.agent === 'all' || e.agent === agent));
108
+ console.log('\npromptCHMOD:');
109
+ if (xOff.length) console.log(` Reference only (do not execute): ${xOff.map(e => e.path).join(', ')}`);
110
+ if (xOn.length) console.log(` Authoritative (execute as instructions): ${xOn.map(e => e.path).join(', ')}`);
111
+ }
112
+
113
+ // Trigger presence heartbeat
114
+ if (agent) {
115
+ spawnSync('node', [join(root, 'bin', 'nexus.js'), 'checkin', agent], { stdio: 'ignore' });
116
+ }
117
+
118
+ console.log('\nNext:');
119
+ if (scope) {
120
+ console.log(' Start is orientation only, not clearance to edit.');
121
+ console.log(` Read ${continuityPath} and latest ${scope.dir}/memories entry.`);
122
+ console.log(` Then: pick work -> nexus claim <path> ${agent} "intent" -> work only there -> nexus release <path> "message".`);
123
+ } else {
124
+ console.log(' Set NEXUS_AGENT or run `nexus start --agent @agy|@claude|@codex|@gemini`, then restart before claiming work.');
125
+ }
126
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * nexus status — pretty-print the current blackboard
3
+ */
4
+
5
+ import { readBoard } from '../lib/blackboard.js';
6
+ import { listLocks } from '../lib/lockManager.js';
7
+ import { getConfig } from '../lib/config.js';
8
+ import { spawnSync } from 'child_process';
9
+
10
+ export default function status(args) {
11
+ const config = getConfig();
12
+ const locks = listLocks();
13
+ const generatedArtifacts = scanGeneratedArtifacts(config.root);
14
+
15
+ if (locks.length === 0) {
16
+ console.log('🐝 No active locks. The swarm is idle.');
17
+ printGeneratedArtifacts(generatedArtifacts);
18
+ return;
19
+ }
20
+
21
+ console.log(`🐝 NEXUS STATUS — ${locks.length} active lock(s)\n`);
22
+
23
+ // Read the blackboard for agent info
24
+ const board = readBoard();
25
+ const boardLines = board.split('\n').filter(l => l.includes('🔒'));
26
+
27
+ for (const lock of locks) {
28
+ const ageStr = lock.age !== null ? formatAge(lock.age) : '??';
29
+ const stale = lock.age !== null && lock.age >= config.staleThreshold;
30
+
31
+ // Find matching board line for agent info
32
+ const boardLine = boardLines.find(l => l.includes(lock.target));
33
+ const agentMatch = boardLine?.match(/Locked by \*\*(.+?)\*\*/);
34
+ const agent = agentMatch ? agentMatch[1] : 'unknown';
35
+
36
+ const staleTag = stale ? ' ⚠️ STALE' : '';
37
+ console.log(` 🔒 ${lock.target}`);
38
+ console.log(` Agent: ${agent} | Age: ${ageStr}${staleTag}`);
39
+ }
40
+
41
+ console.log('');
42
+ printGeneratedArtifacts(generatedArtifacts);
43
+ }
44
+
45
+ function formatAge(seconds) {
46
+ if (seconds < 60) return `${seconds}s`;
47
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
48
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
49
+ }
50
+
51
+ function printGeneratedArtifacts(artifacts) {
52
+ if (artifacts.length === 0) return;
53
+
54
+ console.log('Generated-looking artifacts need owner decision:');
55
+ for (const artifact of artifacts) {
56
+ console.log(` • ${artifact}`);
57
+ }
58
+ console.log(' Decide keep/delete/ignore, or claim and release intentionally.');
59
+ }
60
+
61
+ function scanGeneratedArtifacts(root) {
62
+ const result = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'], {
63
+ cwd: root,
64
+ encoding: 'utf-8',
65
+ stdio: 'pipe',
66
+ });
67
+ if (result.status !== 0) return [];
68
+
69
+ const seen = new Set();
70
+ const artifacts = [];
71
+ for (const line of result.stdout.split('\n')) {
72
+ if (!line.startsWith('?? ')) continue;
73
+ const file = parseGitStatusPath(line.slice(3).trim());
74
+ const ownerPath = generatedArtifactOwnerPath(file);
75
+ if (!ownerPath || seen.has(ownerPath)) continue;
76
+ seen.add(ownerPath);
77
+ artifacts.push(ownerPath);
78
+ }
79
+
80
+ return artifacts;
81
+ }
82
+
83
+ function generatedArtifactOwnerPath(file) {
84
+ const normalized = file.replace(/\\/g, '/');
85
+ if (/(^|\/)(dist|build|coverage|tmp|temp|exports?|reports?|ledgers?|screenshots?)(\/|$)/i.test(normalized)) {
86
+ return firstPathSegment(normalized);
87
+ }
88
+ if (/(^|\/)[^/]*\bcopy\b[^/]*(\/|$)/i.test(normalized)) {
89
+ return firstPathSegment(normalized);
90
+ }
91
+ if (/\.(png|jpe?g|gif|webp|pdf|log|tmp)$/i.test(normalized)) {
92
+ return normalized;
93
+ }
94
+ return '';
95
+ }
96
+
97
+ function parseGitStatusPath(file) {
98
+ if (!file.startsWith('"') || !file.endsWith('"')) return file;
99
+
100
+ try {
101
+ return JSON.parse(file);
102
+ } catch {
103
+ return file.slice(1, -1);
104
+ }
105
+ }
106
+
107
+ function firstPathSegment(file) {
108
+ return file.split('/')[0];
109
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse hook — auto-backup databases before migration commands.
4
+ * Reads Claude Code tool input from stdin, detects migration patterns,
5
+ * runs `nexus db backup --auto` if matched, then exits 0 (allow).
6
+ * Philosophy: backup-then-allow, never block.
7
+ */
8
+
9
+ import { readFileSync } from 'fs';
10
+ import { spawnSync } from 'child_process';
11
+
12
+ const MIGRATION_PATTERNS = /\b(migrate|db:migrate|alembic\s+upgrade|flyway|sequelize.*migrate|knex.*migrate|prisma\s+migrate)\b/i;
13
+
14
+ let input;
15
+ try {
16
+ input = JSON.parse(readFileSync('/dev/stdin', 'utf-8'));
17
+ } catch {
18
+ process.exit(0);
19
+ }
20
+
21
+ const command = input?.tool_input?.command || '';
22
+
23
+ if (MIGRATION_PATTERNS.test(command)) {
24
+ console.log('[nexus db] Migration detected — running backup before proceeding...');
25
+ const result = spawnSync('nexus', ['db', 'backup', '--auto'], {
26
+ stdio: 'inherit',
27
+ shell: true,
28
+ cwd: process.cwd(),
29
+ });
30
+ if (result.status !== 0) {
31
+ console.error('[nexus db] Backup failed — proceeding anyway (check .nexus/db-backups/)');
32
+ }
33
+ }
34
+
35
+ process.exit(0);
@@ -0,0 +1,61 @@
1
+ export const AGENT_SCOPES = {
2
+ '@agy': {
3
+ label: 'Antigravity',
4
+ dir: '.agy',
5
+ entrypoint: '.agy/AGENTS.md',
6
+ continuity: '.agy/CONTINUITY.md',
7
+ memoryIndex: '.agy/memories/INDEX.md',
8
+ memoryDir: '.agy/memories',
9
+ aliases: ['agy', 'antigravity'],
10
+ },
11
+ '@claude': {
12
+ label: 'Claude',
13
+ dir: '.claude',
14
+ entrypoint: '.claude/CLAUDE.md',
15
+ continuity: '.claude/CONTINUITY.md',
16
+ memoryIndex: '.claude/memories/INDEX.md',
17
+ memoryDir: '.claude/memories',
18
+ aliases: ['claude'],
19
+ },
20
+ '@codex': {
21
+ label: 'Codex',
22
+ dir: '.codex',
23
+ entrypoint: '.codex/AGENTS.md',
24
+ continuity: '.codex/CONTINUITY.md',
25
+ memoryIndex: '.codex/memories/INDEX.md',
26
+ memoryDir: '.codex/memories',
27
+ aliases: ['codex'],
28
+ },
29
+ '@gemini': {
30
+ label: 'Gemini',
31
+ dir: '.gemini',
32
+ entrypoint: '.gemini/GEMINI.md',
33
+ continuity: '.gemini/CONTINUITY.md',
34
+ memoryIndex: '.gemini/memories/INDEX.md',
35
+ memoryDir: '.gemini/memories',
36
+ aliases: ['gemini'],
37
+ },
38
+ };
39
+
40
+ export const CANONICAL_MODEL_HANDLES = Object.keys(AGENT_SCOPES);
41
+ export const CANONICAL_MODEL_HANDLES_TEXT = `${CANONICAL_MODEL_HANDLES.slice(0, -1).join(', ')}, or ${CANONICAL_MODEL_HANDLES.at(-1)}`;
42
+ export const CANONICAL_MODEL_HANDLE_SET = new Set(CANONICAL_MODEL_HANDLES);
43
+ export const AGENT_SCOPE_ENTRIES = Object.entries(AGENT_SCOPES);
44
+ export const AGENT_SCOPE_LIST = Object.values(AGENT_SCOPES);
45
+
46
+ export function normalizeAgentHandle(agent) {
47
+ if (!agent) return null;
48
+ const normalized = agent.toLowerCase();
49
+ if (AGENT_SCOPES[normalized]) return normalized;
50
+
51
+ for (const [handle, scope] of AGENT_SCOPE_ENTRIES) {
52
+ if (scope.aliases.some((alias) => normalized.includes(alias))) return handle;
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ export function hasAgentAlias(value) {
59
+ const normalized = value.toLowerCase();
60
+ return AGENT_SCOPE_LIST.some((scope) => scope.aliases.some((alias) => normalized.includes(alias)));
61
+ }