@inkobytes/nexus 1.0.7 → 1.1.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.
- package/CHANGELOG.md +18 -0
- package/README.md +96 -32
- package/bin/nexus.js +11 -1
- package/docs/hooks.md +94 -0
- package/docs/nexus-dynamic-governed-loops.md +135 -0
- package/nexus-dashboard/index.html +14 -0
- package/package.json +4 -2
- package/skills/nexus/SKILL.md +10 -4
- package/src/commands/chmod.js +7 -3
- package/src/commands/claim.js +3 -0
- package/src/commands/dashboard.js +17 -7
- package/src/commands/db.js +41 -17
- package/src/commands/doctor.js +95 -189
- package/src/commands/halt.js +72 -0
- package/src/commands/hooks.js +305 -0
- package/src/commands/init.js +7 -172
- package/src/commands/next.js +3 -0
- package/src/commands/release.js +70 -3
- package/src/commands/resume.js +29 -0
- package/src/lib/config.js +9 -0
- package/src/lib/permissions.js +6 -0
- package/src/lib/protocolText.js +196 -0
package/src/commands/claim.js
CHANGED
|
@@ -11,6 +11,7 @@ import { join } from 'path';
|
|
|
11
11
|
import { cwd } from 'process';
|
|
12
12
|
import { normalizeTarget } from '../lib/pathSafety.js';
|
|
13
13
|
import { CANONICAL_MODEL_HANDLE_SET, CANONICAL_MODEL_HANDLES_TEXT, hasAgentAlias } from '../lib/agentScopes.js';
|
|
14
|
+
import { refuseIfHalted } from './halt.js';
|
|
14
15
|
|
|
15
16
|
const CORE_FILES = [
|
|
16
17
|
'_NEXUS_CONSTITUTION.md',
|
|
@@ -46,6 +47,8 @@ function readFlag(args, name) {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export default function claim(args) {
|
|
50
|
+
refuseIfHalted('claim');
|
|
51
|
+
|
|
49
52
|
const positional = [...args];
|
|
50
53
|
|
|
51
54
|
const agentFlag = readFlag(positional, '--agent').trim();
|
|
@@ -11,6 +11,7 @@ import { join } from 'path';
|
|
|
11
11
|
import { getConfig } from '../lib/config.js';
|
|
12
12
|
import { listLocks } from '../lib/lockManager.js';
|
|
13
13
|
import { readLedgerEntries } from './ledger.js';
|
|
14
|
+
import { getHalt } from './halt.js';
|
|
14
15
|
|
|
15
16
|
const DEFAULT_PORT = 13787;
|
|
16
17
|
const MAX_PORT_SEARCH = 30;
|
|
@@ -24,7 +25,7 @@ export default function dashboard(args) {
|
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
serveDashboard(resolveDashboardPort(args));
|
|
28
|
+
serveDashboard(resolveDashboardPort(args), resolveDashboardHost(args));
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function buildSnapshot() {
|
|
@@ -60,6 +61,7 @@ export function buildSnapshot() {
|
|
|
60
61
|
return {
|
|
61
62
|
generatedAt: new Date().toISOString(),
|
|
62
63
|
repo: config.root,
|
|
64
|
+
halt: getHalt(),
|
|
63
65
|
branch: git.branch,
|
|
64
66
|
dirtyFiles: git.files,
|
|
65
67
|
health: getHealth(config),
|
|
@@ -76,7 +78,7 @@ export function buildSnapshot() {
|
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function serveDashboard(port) {
|
|
81
|
+
function serveDashboard(port, host) {
|
|
80
82
|
const clients = new Set();
|
|
81
83
|
const server = createServer((req, res) => {
|
|
82
84
|
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
@@ -133,7 +135,13 @@ function serveDashboard(port) {
|
|
|
133
135
|
}, 2000);
|
|
134
136
|
|
|
135
137
|
server.on('close', () => clearInterval(interval));
|
|
136
|
-
listenOnAvailablePort(server, port, port === DEFAULT_PORT);
|
|
138
|
+
listenOnAvailablePort(server, port, port === DEFAULT_PORT, host);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The dashboard has no auth, so network exposure is opt-in: localhost unless
|
|
142
|
+
// the human passes --lan.
|
|
143
|
+
export function resolveDashboardHost(args) {
|
|
144
|
+
return args.includes('--lan') ? '0.0.0.0' : '127.0.0.1';
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
export function resolveDashboardPort(args) {
|
|
@@ -146,7 +154,7 @@ export function resolveDashboardPort(args) {
|
|
|
146
154
|
return value;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
function listenOnAvailablePort(server, port, canSearch) {
|
|
157
|
+
function listenOnAvailablePort(server, port, canSearch, host = '127.0.0.1') {
|
|
150
158
|
let attempts = 0;
|
|
151
159
|
|
|
152
160
|
const tryListen = (candidate) => {
|
|
@@ -159,11 +167,13 @@ function listenOnAvailablePort(server, port, canSearch) {
|
|
|
159
167
|
throw err;
|
|
160
168
|
});
|
|
161
169
|
|
|
162
|
-
server.listen(candidate,
|
|
170
|
+
server.listen(candidate, host, () => {
|
|
163
171
|
const moved = candidate !== port ? ` (default ${port} was busy)` : '';
|
|
164
172
|
console.log(`Nexus dashboard listening at http://127.0.0.1:${candidate}${moved}`);
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
if (host === '0.0.0.0') {
|
|
174
|
+
for (const url of getLanUrls(candidate)) {
|
|
175
|
+
console.log(`Local network: ${url}`);
|
|
176
|
+
}
|
|
167
177
|
}
|
|
168
178
|
console.log('Press Ctrl+C to stop.');
|
|
169
179
|
});
|
package/src/commands/db.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* nexus db schedule Show cron setup instructions
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
12
|
-
import { join,
|
|
11
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, readFileSync, writeFileSync, openSync, closeSync } from 'fs';
|
|
12
|
+
import { join, dirname, relative } from 'path';
|
|
13
13
|
import { cwd, env } from 'process';
|
|
14
14
|
import { spawnSync } from 'child_process';
|
|
15
15
|
|
|
@@ -38,7 +38,7 @@ function detectDatabases(root) {
|
|
|
38
38
|
const stat = statSync(full);
|
|
39
39
|
if (stat.isDirectory()) { scanDir(full, depth + 1); continue; }
|
|
40
40
|
if (/\.(sqlite|sqlite3|db)$/.test(entry)) {
|
|
41
|
-
dbs.push({ type: 'sqlite', path: full, name: entry });
|
|
41
|
+
dbs.push({ type: 'sqlite', path: full, relPath: relative(root, full), name: entry });
|
|
42
42
|
}
|
|
43
43
|
} catch { /* skip unreadable */ }
|
|
44
44
|
}
|
|
@@ -58,8 +58,12 @@ function detectDatabases(root) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function backupSqlite(db, backupPath) {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Mirror the repo-relative path inside the backup so same-named DBs in
|
|
62
|
+
// different directories cannot overwrite each other.
|
|
63
|
+
const dest = join(backupPath, db.relPath);
|
|
64
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
65
|
+
copyFileSync(db.path, dest);
|
|
66
|
+
return db.relPath;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
function backupPostgres(db, backupPath) {
|
|
@@ -72,10 +76,19 @@ function backupPostgres(db, backupPath) {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
function backupMysql(db, backupPath) {
|
|
79
|
+
// DATABASE_URL comes from .env and is attacker-influenced; pass it as a
|
|
80
|
+
// literal argument and redirect in Node — never through a shell string.
|
|
75
81
|
const outFile = join(backupPath, 'dump.sql');
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
const out = openSync(outFile, 'w');
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = spawnSync('mysqldump', [db.url], {
|
|
86
|
+
encoding: 'utf-8', stdio: ['ignore', out, 'pipe'],
|
|
87
|
+
});
|
|
88
|
+
} finally {
|
|
89
|
+
closeSync(out);
|
|
90
|
+
}
|
|
91
|
+
if (result.error) throw new Error(`mysqldump failed: ${result.error.message}`);
|
|
79
92
|
if (result.status !== 0) throw new Error(`mysqldump failed: ${result.stderr}`);
|
|
80
93
|
return 'dump.sql';
|
|
81
94
|
}
|
|
@@ -121,7 +134,7 @@ function runBackup(root, auto = false) {
|
|
|
121
134
|
if (db.type === 'sqlite') file = backupSqlite(db, backupPath);
|
|
122
135
|
if (db.type === 'postgres') file = backupPostgres(db, backupPath);
|
|
123
136
|
if (db.type === 'mysql') file = backupMysql(db, backupPath);
|
|
124
|
-
results.push({ db: db.name, type: db.type, file, ok: true });
|
|
137
|
+
results.push({ db: db.name, path: db.relPath, type: db.type, file, ok: true });
|
|
125
138
|
console.log(`[nexus db] ✓ ${db.type} ${db.name} → ${BACKUP_DIR}/${stamp}/${file}`);
|
|
126
139
|
} catch (err) {
|
|
127
140
|
results.push({ db: db.name, type: db.type, ok: false, error: err.message });
|
|
@@ -214,14 +227,18 @@ function runRestore(root, stamp) {
|
|
|
214
227
|
const backupFile = join(backupPath, entry.file);
|
|
215
228
|
|
|
216
229
|
if (entry.type === 'sqlite') {
|
|
217
|
-
//
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
// Older manifests stored only the basename; fall back so they stay restorable.
|
|
231
|
+
const rel = entry.path || entry.db;
|
|
232
|
+
const target = join(root, rel);
|
|
233
|
+
if (!existsSync(backupFile)) {
|
|
234
|
+
console.error(` ✗ sqlite ${entry.db}: backup file missing — expected ${backupFile}`);
|
|
235
|
+
process.exitCode = 1;
|
|
236
|
+
} else if (!existsSync(dirname(target))) {
|
|
237
|
+
console.error(` ✗ sqlite ${entry.db}: original directory is gone (${dirname(rel)}/) — restore manually from ${backupFile}`);
|
|
238
|
+
process.exitCode = 1;
|
|
223
239
|
} else {
|
|
224
|
-
|
|
240
|
+
copyFileSync(backupFile, target);
|
|
241
|
+
console.log(` ✓ sqlite ${entry.db} → ${rel}`);
|
|
225
242
|
}
|
|
226
243
|
}
|
|
227
244
|
|
|
@@ -235,7 +252,14 @@ function runRestore(root, stamp) {
|
|
|
235
252
|
if (entry.type === 'mysql') {
|
|
236
253
|
const url = env.DATABASE_URL || env.MYSQL_URL;
|
|
237
254
|
if (!url) { console.log(` ✗ mysql: DATABASE_URL not set`); continue; }
|
|
238
|
-
|
|
255
|
+
// Same injection surface as backupMysql: literal argument, fd redirection.
|
|
256
|
+
const input = openSync(backupFile, 'r');
|
|
257
|
+
let result;
|
|
258
|
+
try {
|
|
259
|
+
result = spawnSync('mysql', [url], { stdio: [input, 'inherit', 'inherit'] });
|
|
260
|
+
} finally {
|
|
261
|
+
closeSync(input);
|
|
262
|
+
}
|
|
239
263
|
console.log(result.status === 0 ? ` ✓ mysql restored` : ` ✗ mysql restore failed`);
|
|
240
264
|
}
|
|
241
265
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -10,39 +10,19 @@ import { listLocks } from '../lib/lockManager.js';
|
|
|
10
10
|
import { getConfig } from '../lib/config.js';
|
|
11
11
|
import { AGENT_SCOPE_LIST } from '../lib/agentScopes.js';
|
|
12
12
|
import { DEFAULT_MATRIX, loadPermissions, getChmodPath } from '../lib/permissions.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'December',
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const START_MARKER = '<!-- NEXUS-AGENT-PROTOCOL:START -->';
|
|
30
|
-
const END_MARKER = '<!-- NEXUS-AGENT-PROTOCOL:END -->';
|
|
31
|
-
|
|
32
|
-
const CONTINUITY_TEMPLATE = `# CONTINUITY
|
|
33
|
-
Goal: Project setup
|
|
34
|
-
State: Planning
|
|
35
|
-
|
|
36
|
-
Now: Initial Nexus setup
|
|
37
|
-
Next: Confirm first task
|
|
38
|
-
Blockers: None
|
|
39
|
-
Decisions:
|
|
40
|
-
- Nexus manages swarm coordination
|
|
41
|
-
- Continuity and memories are agent-local
|
|
42
|
-
Files:
|
|
43
|
-
- _NEXUS_QUEUE.md
|
|
44
|
-
- _NEXUS_STANDUP.md
|
|
45
|
-
`;
|
|
13
|
+
import {
|
|
14
|
+
CONTINUITY_TEMPLATE,
|
|
15
|
+
END_MARKER,
|
|
16
|
+
MEMORY_INDEX_GUARDRAIL,
|
|
17
|
+
MEMORY_INDEX_TEMPLATE,
|
|
18
|
+
REQUIRED_CONTEXT_READ,
|
|
19
|
+
SKILL_CONTEXT_GUARDRAIL,
|
|
20
|
+
START_MARKER,
|
|
21
|
+
currentMemoryMonthFolder,
|
|
22
|
+
fullEntrypoint,
|
|
23
|
+
protocolBlock,
|
|
24
|
+
} from '../lib/protocolText.js';
|
|
25
|
+
import { HOOK_AGENT_CONFIGS, hookStatus } from './hooks.js';
|
|
46
26
|
|
|
47
27
|
const LOCAL_DECISIONS_TEMPLATE = `# Decisions
|
|
48
28
|
|
|
@@ -53,148 +33,6 @@ const LOCAL_GITIGNORE_LINES = ['DECISIONS.md', 'docs-priv/', '.nexus/presence/']
|
|
|
53
33
|
const STANDUP_FORMAT_GUIDANCE = 'YYYY-MM-DD HH:MM AM/PM @agent [STATUS]: message';
|
|
54
34
|
const STANDUP_RULES_LINE = `*Rules: Append new entries at the bottom. One line per message. Use \`${STANDUP_FORMAT_GUIDANCE}\` so relevance is visible. Use 🧵 for long discussions.*`;
|
|
55
35
|
|
|
56
|
-
const MEMORY_INDEX_TEMPLATE = `# Memory Index
|
|
57
|
-
|
|
58
|
-
Newest first, max 10 visible entries.
|
|
59
|
-
|
|
60
|
-
Format:
|
|
61
|
-
|
|
62
|
-
- YYYY-Month/YYYY-MM-DD-HHMM-topic.md - short session label
|
|
63
|
-
|
|
64
|
-
Entries live in month folders from the start, for example:
|
|
65
|
-
|
|
66
|
-
- \`2026-January/2026-01-15-1030-project-setup.md\`
|
|
67
|
-
- \`2026-February/2026-02-01-0900-debug-session.md\`
|
|
68
|
-
|
|
69
|
-
This keeps monthly review simple: ask an agent to read one month folder and summarize the Markdown files.
|
|
70
|
-
|
|
71
|
-
`;
|
|
72
|
-
|
|
73
|
-
function currentMemoryMonthFolder(now = new Date()) {
|
|
74
|
-
return `${now.getFullYear()}-${MONTH_NAMES[now.getMonth()]}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function protocolBlock(agent) {
|
|
78
|
-
return `${START_MARKER}
|
|
79
|
-
|
|
80
|
-
## Nexus Project Protocol
|
|
81
|
-
|
|
82
|
-
This project uses Nexus for multi-agent coordination.
|
|
83
|
-
|
|
84
|
-
### Start Here
|
|
85
|
-
|
|
86
|
-
1. Read \`_NEXUS_CONSTITUTION.md\`.
|
|
87
|
-
2. Read \`_NEXUS_QUEUE.md\` for executable priorities.
|
|
88
|
-
3. Read \`_NEXUS_STANDUP.md\` for comms, decisions, and completion notes.
|
|
89
|
-
4. Read \`USER.md\` if present for local human preferences.
|
|
90
|
-
5. Read \`${agent.continuity}\` for current session state.
|
|
91
|
-
6. Read \`${agent.memoryIndex}\` and the latest memory entry when resync is needed.
|
|
92
|
-
|
|
93
|
-
### Nexus Rules
|
|
94
|
-
|
|
95
|
-
- Claim before editing shared project files: \`nexus claim <path> @Agent "intent"\`.
|
|
96
|
-
- Nexus is agent-native and file-native, not human-native: optimize for concurrency and rollback, not feature-commit aesthetics.
|
|
97
|
-
- Release each claimed file as soon as it reaches a coherent checkpoint.
|
|
98
|
-
- Never hold claims just to bundle a prettier feature commit; that blocks other agents.
|
|
99
|
-
- Release finished work through Nexus: \`nexus release <path> "commit message"\`.
|
|
100
|
-
- Use \`nexus next @Agent\` for the next safe queue task.
|
|
101
|
-
- Do not free-roam into unassigned or \`Auto-flow: no\` work without user approval.
|
|
102
|
-
- Direct user instruction can override queue order, but not claim/release, data, security, or approval gates.
|
|
103
|
-
- If no safe task remains, announce \`Standby\` with what you are waiting for, then stop until user input, queue change, or explicit assignment.
|
|
104
|
-
|
|
105
|
-
### Current File State
|
|
106
|
-
|
|
107
|
-
- Treat previous chat context, cached model memory, and earlier reads as stale when file contents matter.
|
|
108
|
-
- Before claiming what a file says, making edits, or judging current state, read the file from disk with a fresh command.
|
|
109
|
-
- Treat \`nexus claim\` as the atomic lock-and-read boundary and its output as fresh file state for the claimed path.
|
|
110
|
-
- If you read a shared file before claiming it, treat that read as stale after claim succeeds.
|
|
111
|
-
- If another agent or tool may have touched the file since your last read, re-read it before editing.
|
|
112
|
-
- If a claim appears stale, do not edit through it; run \`nexus status\` or \`nexus doctor\`, then clean only when ownership is clearly abandoned.
|
|
113
|
-
|
|
114
|
-
### Drills
|
|
115
|
-
|
|
116
|
-
Drill guidance is defined in \`_NEXUS_CONSTITUTION.md\`.
|
|
117
|
-
If the situation resembles a drill, use that drill before acting.
|
|
118
|
-
|
|
119
|
-
### Delegated Work
|
|
120
|
-
|
|
121
|
-
- Lead agents own the repo effects of their subagents, tools, and parallel workers.
|
|
122
|
-
- Claim the full path scope before delegating shared-file work.
|
|
123
|
-
- Give subagents the claimed path, intent, non-goals, and boundaries.
|
|
124
|
-
- Re-read affected files after subagent work before final edits, release, or current-state claims.
|
|
125
|
-
- Mention delegated work in release or \`nexus standup\` notes when it affected files, tests, or risk.
|
|
126
|
-
|
|
127
|
-
### Git Write Safety
|
|
128
|
-
|
|
129
|
-
- Before git writes, verify \`pwd\`, repo root, branch/status, and remotes.
|
|
130
|
-
- Stop if they do not match the requested project.
|
|
131
|
-
- Never infer from similar folder names or cached context.
|
|
132
|
-
- Require explicit confirmation before push/force-push, main/master, remote changes, or deletes.
|
|
133
|
-
- To remove private agent files from git, untrack them; do not delete local folders.
|
|
134
|
-
- Agent instruction files are shared protocol files; normal edits require claim/release, while \`nexus doctor --fix\` may update managed protocol blocks after user approval.
|
|
135
|
-
- Agents work inside assigned work zones. If a change crosses work-zone boundaries or alters a shared contract another zone may depend on, announce it in \`_NEXUS_STANDUP.md\` before release and ask if coordination is needed.
|
|
136
|
-
|
|
137
|
-
### Supply-Chain Safety
|
|
138
|
-
|
|
139
|
-
- Do not install third-party packages that have existed for less than 14 days.
|
|
140
|
-
- Before adding a new dependency, verify the package creation date and the specific version publish date.
|
|
141
|
-
- If the package or version is younger than 14 days, or either date cannot be verified, stop and ask the user.
|
|
142
|
-
- Run \`nexus doctor\` before installs; review any Security findings before running package scripts.
|
|
143
|
-
- \`nexus doctor\` is cheap, local, and idempotent.
|
|
144
|
-
- If \`nexus doctor\` reports Security, Package Privacy, Git Privacy, or supply-chain findings, stop and report before fixing or installing.
|
|
145
|
-
- Treat install hooks and scripts with network commands, webhooks, raw sockets, SSH, or secret-looking variables as human-review only.
|
|
146
|
-
- Prefer built-in runtime APIs and existing project dependencies when they fit.
|
|
147
|
-
|
|
148
|
-
### Agent-Local Files
|
|
149
|
-
|
|
150
|
-
\`${agent.continuity}\` and \`${agent.memoryIndex}\` are agent-local handoff files.
|
|
151
|
-
They are exempt from Nexus claim/release unless the user says otherwise.
|
|
152
|
-
|
|
153
|
-
### Memory Flow
|
|
154
|
-
|
|
155
|
-
- On session start, read \`${agent.memoryIndex}\`.
|
|
156
|
-
- If the index has entries, read the newest \`${agent.memoryDir}/YYYY-Month/YYYY-MM-DD-HHMM-topic.md\` entry.
|
|
157
|
-
- Durable architecture and protocol decisions belong in \`DECISIONS.md\`; mention them in \`_NEXUS_STANDUP.md\` only when active agents need to coordinate around them.
|
|
158
|
-
- Memory entries are session handoffs.
|
|
159
|
-
- When writing your own memory entry, create the current month folder under \`${agent.memoryDir}\` if it is missing.
|
|
160
|
-
- Do not create or repair other agents' memory folders manually; use \`nexus doctor --fix\` for broad scaffold repair.
|
|
161
|
-
- On session end, pause, or checkpoint request:
|
|
162
|
-
1. Run \`nexus checkout @${agent.aliases[0]}\` to clear your presence heartbeat.
|
|
163
|
-
2. Create one new memory file: \`${agent.memoryDir}/YYYY-Month/YYYY-MM-DD-HHMM-topic.md\`.
|
|
164
|
-
- Add the newest file to the top of \`${agent.memoryIndex}\`.
|
|
165
|
-
- Keep the index to the 10 newest visible entries.
|
|
166
|
-
- For monthly review, read one month folder such as \`${agent.memoryDir}/2026-January/\` and summarize the Markdown files.
|
|
167
|
-
|
|
168
|
-
Memory entry format:
|
|
169
|
-
|
|
170
|
-
\`\`\`markdown
|
|
171
|
-
# YYYY-MM-DD-HHMM - <topic>
|
|
172
|
-
|
|
173
|
-
## Session Summary
|
|
174
|
-
- What we worked on: [<=50 words]
|
|
175
|
-
- What got done: [bullet list, max 5]
|
|
176
|
-
- Where we stopped: [exact state, <=30 words]
|
|
177
|
-
|
|
178
|
-
## Next Session Needs
|
|
179
|
-
- Immediate next task: [<=20 words]
|
|
180
|
-
- Blockers: [None, or list]
|
|
181
|
-
- Open questions: [if any]
|
|
182
|
-
|
|
183
|
-
## Context to Carry
|
|
184
|
-
- Key decisions made: [max 3 bullets]
|
|
185
|
-
- Files touched: [max 5 paths]
|
|
186
|
-
- Gotchas/warnings: [anything next session should watch for]
|
|
187
|
-
\`\`\`
|
|
188
|
-
|
|
189
|
-
${END_MARKER}
|
|
190
|
-
`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function fullEntrypoint(agent) {
|
|
194
|
-
return `# ${agent.label} Agent Guide
|
|
195
|
-
|
|
196
|
-
${protocolBlock(agent)}`;
|
|
197
|
-
}
|
|
198
36
|
|
|
199
37
|
function upsertProtocolBlock(content, block) {
|
|
200
38
|
const cleanContent = removeUnmanagedProtocolBlock(content);
|
|
@@ -347,6 +185,7 @@ function repairStandupGuidance(content) {
|
|
|
347
185
|
export default function doctor(args) {
|
|
348
186
|
const fix = args.includes('--fix');
|
|
349
187
|
const json = args.includes('--json');
|
|
188
|
+
const checkHooks = args.includes('--hooks');
|
|
350
189
|
const root = cwd();
|
|
351
190
|
const colors = createColors();
|
|
352
191
|
const sections = {
|
|
@@ -361,8 +200,10 @@ export default function doctor(args) {
|
|
|
361
200
|
Memories: [],
|
|
362
201
|
Locks: [],
|
|
363
202
|
'Generated Artifacts': [],
|
|
203
|
+
Hooks: [],
|
|
364
204
|
promptCHMOD: [],
|
|
365
205
|
'Queue Authorship': [],
|
|
206
|
+
'Loop Readiness': [],
|
|
366
207
|
};
|
|
367
208
|
const changes = [];
|
|
368
209
|
const config = getConfig(root);
|
|
@@ -423,6 +264,26 @@ export default function doctor(args) {
|
|
|
423
264
|
sections['Generated Artifacts'].push(issue);
|
|
424
265
|
}
|
|
425
266
|
|
|
267
|
+
if (checkHooks) {
|
|
268
|
+
for (const agent of Object.keys(HOOK_AGENT_CONFIGS)) {
|
|
269
|
+
const status = hookStatus(agent);
|
|
270
|
+
if (status.status === 'current') {
|
|
271
|
+
sections.Hooks.push({
|
|
272
|
+
issue: `${agent} Nexus hook is installed`,
|
|
273
|
+
fix: 'No action needed.',
|
|
274
|
+
ok: true,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const fixHint = `Run \`nexus hooks install --agent ${agent}\`${status.status === 'foreign' ? ' after reviewing the existing hook, or add `--force` to replace it' : ''}.`;
|
|
280
|
+
sections.Hooks.push({
|
|
281
|
+
issue: `${agent} Nexus hook is ${status.status} at ${status.path}`,
|
|
282
|
+
fix: fixHint,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
426
287
|
if (!ensureFile(join(root, 'DECISIONS.md'), LOCAL_DECISIONS_TEMPLATE, fix, changes)) {
|
|
427
288
|
sections['Nexus Files'].push({
|
|
428
289
|
issue: 'Missing local DECISIONS.md',
|
|
@@ -692,6 +553,21 @@ export default function doctor(args) {
|
|
|
692
553
|
}
|
|
693
554
|
}
|
|
694
555
|
|
|
556
|
+
// Loop readiness — autonomy above supervised requires a release verify gate
|
|
557
|
+
if (config.autonomy >= 1) {
|
|
558
|
+
if (!config.release.verifyCommand) {
|
|
559
|
+
sections['Loop Readiness'].push({
|
|
560
|
+
issue: `autonomy is ${config.autonomy} but release.verifyCommand is not configured — agents can compound on unverified commits`,
|
|
561
|
+
fix: 'Set release.verifyCommand in .nexus/config.json (e.g. "npm test") or lower autonomy to 0.',
|
|
562
|
+
});
|
|
563
|
+
} else {
|
|
564
|
+
sections['Loop Readiness'].push({
|
|
565
|
+
issue: `autonomy ${config.autonomy} with release verify gate configured (${config.release.verifyCommand})`,
|
|
566
|
+
ok: true,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
695
571
|
for (const relativePath of legacyCheckFiles) {
|
|
696
572
|
const path = join(root, relativePath);
|
|
697
573
|
if (!existsSync(path)) continue;
|
|
@@ -992,7 +868,7 @@ function isNexusProductRepo(root) {
|
|
|
992
868
|
}
|
|
993
869
|
|
|
994
870
|
function repairReadmeProtocolDoc(content) {
|
|
995
|
-
|
|
871
|
+
let next = content
|
|
996
872
|
.replace(
|
|
997
873
|
[
|
|
998
874
|
'- [ ] TASK/Codex: Add doctor stale-lock category',
|
|
@@ -1021,10 +897,6 @@ function repairReadmeProtocolDoc(content) {
|
|
|
1021
897
|
' - Notes: Add a doctor section for stale locks with tests and clear fix guidance.',
|
|
1022
898
|
].join('\n'),
|
|
1023
899
|
)
|
|
1024
|
-
.replace(
|
|
1025
|
-
'Keep items dashboard-friendly: include `Id`, `Epic`, `Status`, `Depends on`, `Files`, `Affinity`, `Cost`, `Auto-flow`, and `Notes`. Use `Files` to expose conflict surfaces, `Depends on` for hard blockers, and `Auto-flow: no` when a task needs planning or human approval before an agent grabs it.',
|
|
1026
|
-
'Keep items dashboard-friendly: include `Id`, `Epic`, `Status`, `Depends on`, `Files`, `Affinity`, `Cost`, `Auto-flow`, and `Notes`. Use `Files` to expose conflict surfaces, `Depends on` for hard blockers, and `Auto-flow: no` when a task needs planning or human approval before an agent grabs it. Auto-flow work in `Ready Queue` should also include `Review: approved` and `Approved by: human`, or `doctor` will flag it and `nexus next` may skip it.',
|
|
1027
|
-
)
|
|
1028
900
|
.replace(
|
|
1029
901
|
[
|
|
1030
902
|
'1. Run `nexus start` when entering an existing repo; it does not replace claim/release.',
|
|
@@ -1039,25 +911,39 @@ function repairReadmeProtocolDoc(content) {
|
|
|
1039
911
|
'1. Run `nexus start` when entering an existing repo; it does not replace claim/release.',
|
|
1040
912
|
'2. Read `_NEXUS_CONSTITUTION.md`.',
|
|
1041
913
|
'3. Read `USER.md` when present.',
|
|
1042
|
-
|
|
914
|
+
`4. ${REQUIRED_CONTEXT_READ}`,
|
|
1043
915
|
'5. Read `_NEXUS_QUEUE.md` before taking follow-on work.',
|
|
1044
916
|
'6. Claim before touching shared project files.',
|
|
1045
917
|
'7. Release each claimed tracked file as soon as it reaches a coherent checkpoint.',
|
|
1046
918
|
'8. Use `nexus next @Agent` instead of free-roaming.',
|
|
1047
919
|
].join('\n'),
|
|
1048
920
|
)
|
|
1049
|
-
.replace(
|
|
1050
|
-
'Agent-local continuity and memory files are exempt from claim/release unless the human says otherwise.',
|
|
1051
|
-
'Agent-local continuity and memory files are exempt from claim/release unless the human says otherwise.\n\nNexus is agent-native and file-native, not human-native: optimize for concurrency and rollback, not feature-commit aesthetics. Do not hold claims to bundle related work into prettier feature commits; that blocks other agents waiting on files.',
|
|
1052
|
-
)
|
|
1053
921
|
.replace(
|
|
1054
922
|
'The CLI is the coordination engine. The skill is the lean playbook for this flow: `start -> claim -> release`.',
|
|
1055
923
|
'The CLI is the coordination engine. The skill is the lean playbook for this flow: `start -> claim -> work -> release -> next`.',
|
|
1056
924
|
);
|
|
925
|
+
|
|
926
|
+
const queueReviewReadme = 'Auto-flow work in `Ready Queue` should also include `Review: approved` and `Approved by: human`, or `doctor` will flag it and `nexus next` may skip it.';
|
|
927
|
+
if (!next.includes(queueReviewReadme)) {
|
|
928
|
+
next = next.replace(
|
|
929
|
+
'Keep items dashboard-friendly: include `Id`, `Epic`, `Status`, `Depends on`, `Files`, `Affinity`, `Cost`, `Auto-flow`, and `Notes`. Use `Files` to expose conflict surfaces, `Depends on` for hard blockers, and `Auto-flow: no` when a task needs planning or human approval before an agent grabs it.',
|
|
930
|
+
`Keep items dashboard-friendly: include \`Id\`, \`Epic\`, \`Status\`, \`Depends on\`, \`Files\`, \`Affinity\`, \`Cost\`, \`Auto-flow\`, and \`Notes\`. Use \`Files\` to expose conflict surfaces, \`Depends on\` for hard blockers, and \`Auto-flow: no\` when a task needs planning or human approval before an agent grabs it. ${queueReviewReadme}`,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const agentNativeReadme = 'Nexus is agent-native and file-native, not human-native: optimize for concurrency and rollback, not feature-commit aesthetics. Do not hold claims to bundle related work into prettier feature commits; that blocks other agents waiting on files.';
|
|
935
|
+
if (!next.includes(agentNativeReadme)) {
|
|
936
|
+
next = next.replace(
|
|
937
|
+
'Agent-local continuity and memory files are exempt from claim/release unless the human says otherwise.',
|
|
938
|
+
`Agent-local continuity and memory files are exempt from claim/release unless the human says otherwise.\n\n${agentNativeReadme}`,
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return next;
|
|
1057
943
|
}
|
|
1058
944
|
|
|
1059
945
|
function repairNexusSkillDoc(content) {
|
|
1060
|
-
|
|
946
|
+
let next = content
|
|
1061
947
|
.replace(
|
|
1062
948
|
[
|
|
1063
949
|
'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.',
|
|
@@ -1085,13 +971,14 @@ function repairNexusSkillDoc(content) {
|
|
|
1085
971
|
].join('\n'),
|
|
1086
972
|
[
|
|
1087
973
|
'8. Treat claim output as current file state. Ignore cached file memory when contents matter.',
|
|
1088
|
-
'9.
|
|
1089
|
-
'10.
|
|
974
|
+
'9. If a hook blocks access because a path is unclaimed, stop and claim that exact path. Do not work around the hook with another command, cached content, or manual git operation.',
|
|
975
|
+
'10. Work only inside the claimed surface and run focused validation.',
|
|
976
|
+
'11. Release each claimed tracked file through Nexus as soon as it reaches a coherent checkpoint:',
|
|
1090
977
|
].join('\n'),
|
|
1091
978
|
)
|
|
1092
979
|
.replace(
|
|
1093
980
|
'```\\n\\n## Queue Items',
|
|
1094
|
-
'```\\n\\
|
|
981
|
+
'```\\n\\n12. Do not hold claims to bundle related work into a prettier feature commit. Nexus is agent-native and file-native: optimize for file availability, rollback safety, and agent throughput.\\n\\n## Queue Items',
|
|
1095
982
|
)
|
|
1096
983
|
.replace(
|
|
1097
984
|
[
|
|
@@ -1111,6 +998,25 @@ function repairNexusSkillDoc(content) {
|
|
|
1111
998
|
'- `Auto-flow: yes` means an agent can grab it after `nexus next`; use `no` when planning or human approval is needed.\n- `Notes` should carry dashboard-useful context, not a whole design doc.',
|
|
1112
999
|
'- `Auto-flow: yes` means an agent can grab it after `nexus next`; use `no` when planning or human approval is needed.\n- Auto-flow work in `Ready Queue` should include `Review: approved` and `Approved by: human`, or `doctor` will flag it and `nexus next` may skip it.\n- `Notes` should carry dashboard-useful context, not a whole design doc.',
|
|
1113
1000
|
);
|
|
1001
|
+
|
|
1002
|
+
if (!next.includes('Continuity is the compaction-safe session ledger; latest memory is required startup/resume context.')) {
|
|
1003
|
+
next = next.replace(
|
|
1004
|
+
'- Agent-local continuity and memory files are claim-exempt unless the user says otherwise.',
|
|
1005
|
+
`- Agent-local continuity and memory files are claim-exempt unless the user says otherwise.\n- ${SKILL_CONTEXT_GUARDRAIL}\n- ${MEMORY_INDEX_GUARDRAIL}`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const mandatoryNote = 'If the user, repo, or hook says Nexus is active, treat this skill as mandatory workflow. It is not optional advice.';
|
|
1010
|
+
if (!next.includes(mandatoryNote)) {
|
|
1011
|
+
next = next.replace('## Loop', `${mandatoryNote}\n\n## Loop`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
next = next.replace(
|
|
1015
|
+
'4. Read continuity and latest memory when present.',
|
|
1016
|
+
`4. ${REQUIRED_CONTEXT_READ}`,
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
return next;
|
|
1114
1020
|
}
|
|
1115
1021
|
|
|
1116
1022
|
const SUSPICIOUS_SCRIPT_PATTERNS = [
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nexus halt "<reason>" — repo-wide circuit breaker.
|
|
3
|
+
* Writes .nexus/HALT; while it exists, claim, release, and next refuse and
|
|
4
|
+
* tell agents to stand by. Any agent or human may halt (an agent that smells
|
|
5
|
+
* swarm-level trouble should be able to stop everyone). Only humans resume —
|
|
6
|
+
* by convention, honored at session level, not mechanically enforced.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { getConfig } from '../lib/config.js';
|
|
12
|
+
|
|
13
|
+
export function getHaltPath() {
|
|
14
|
+
return join(getConfig().root, '.nexus', 'HALT');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getHalt() {
|
|
18
|
+
const path = getHaltPath();
|
|
19
|
+
if (!existsSync(path)) return null;
|
|
20
|
+
try {
|
|
21
|
+
const halt = JSON.parse(readFileSync(path, 'utf-8'));
|
|
22
|
+
return {
|
|
23
|
+
reason: halt.reason || '(no reason recorded)',
|
|
24
|
+
at: halt.at || 'unknown',
|
|
25
|
+
by: halt.by || 'unknown',
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
// A corrupt HALT file still halts; never let a parse error unfreeze the swarm.
|
|
29
|
+
return { reason: '(unreadable HALT file)', at: 'unknown', by: 'unknown' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function refuseIfHalted(command) {
|
|
34
|
+
const halt = getHalt();
|
|
35
|
+
if (!halt) return;
|
|
36
|
+
console.error(`[HALTED] nexus ${command} refused — the swarm is halted.`);
|
|
37
|
+
console.error(` Reason: ${halt.reason}`);
|
|
38
|
+
console.error(` Since: ${halt.at} by ${halt.by}`);
|
|
39
|
+
console.error('Stand by: append a dated standup line noting you are halted, then stop.');
|
|
40
|
+
console.error('Do not work around the halt with other tools. A human lifts it with `nexus resume`.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function halt(args) {
|
|
45
|
+
const reason = (args[0] || '').trim();
|
|
46
|
+
|
|
47
|
+
if (!reason || reason.startsWith('--')) {
|
|
48
|
+
console.error('Usage: nexus halt "<reason>"');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const existing = getHalt();
|
|
53
|
+
if (existing) {
|
|
54
|
+
console.log(`[INFO] Swarm is already halted since ${existing.at} by ${existing.by}: ${existing.reason}`);
|
|
55
|
+
console.log('A human can lift it with `nexus resume`.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const by = process.env.NEXUS_AGENT
|
|
60
|
+
|| (process.env.CLAUDECODE === '1' ? 'agent-session' : 'human');
|
|
61
|
+
|
|
62
|
+
const haltPath = getHaltPath();
|
|
63
|
+
mkdirSync(dirname(haltPath), { recursive: true });
|
|
64
|
+
writeFileSync(haltPath, JSON.stringify({
|
|
65
|
+
reason,
|
|
66
|
+
at: new Date().toISOString(),
|
|
67
|
+
by,
|
|
68
|
+
}, null, 2), 'utf-8');
|
|
69
|
+
|
|
70
|
+
console.log(`[HALT] Swarm halted: ${reason}`);
|
|
71
|
+
console.log('claim, release, and next now refuse repo-wide until a human runs `nexus resume`.');
|
|
72
|
+
}
|