@moreih29/nexus-core 0.16.2 → 0.17.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/assets/capability-matrix.yml +7 -5
- package/assets/hooks/agent-bootstrap/handler.test.ts +18 -17
- package/assets/hooks/agent-bootstrap/handler.ts +20 -7
- package/assets/hooks/post-tool-telemetry/meta.yml +1 -2
- package/assets/hooks/session-init/handler.ts +8 -7
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -1
- package/dist/assets/hooks/agent-bootstrap/handler.js +22 -8
- package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -1
- package/dist/assets/hooks/session-init/handler.d.ts.map +1 -1
- package/dist/assets/hooks/session-init/handler.js +5 -6
- package/dist/assets/hooks/session-init/handler.js.map +1 -1
- package/dist/claude/agents/architect.md +1 -1
- package/dist/claude/agents/designer.md +1 -1
- package/dist/claude/agents/engineer.md +1 -1
- package/dist/claude/agents/lead.md +1 -1
- package/dist/claude/agents/postdoc.md +1 -1
- package/dist/claude/agents/researcher.md +1 -1
- package/dist/claude/agents/reviewer.md +1 -1
- package/dist/claude/agents/strategist.md +1 -1
- package/dist/claude/agents/tester.md +1 -1
- package/dist/claude/agents/writer.md +1 -1
- package/dist/claude/dist/hooks/agent-bootstrap.js +128 -11
- package/dist/claude/dist/hooks/agent-finalize.js +1 -1
- package/dist/claude/dist/hooks/post-tool-telemetry.js +71 -0
- package/dist/claude/dist/hooks/prompt-router.js +1 -1
- package/dist/claude/dist/hooks/session-init.js +14 -1
- package/dist/claude/hooks/hooks.json +12 -0
- package/dist/codex/dist/hooks/agent-bootstrap.js +128 -11
- package/dist/codex/dist/hooks/agent-finalize.js +1 -1
- package/dist/codex/dist/hooks/prompt-router.js +1 -1
- package/dist/codex/dist/hooks/session-init.js +14 -1
- package/dist/hooks/agent-bootstrap.js +128 -11
- package/dist/hooks/agent-finalize.js +1 -1
- package/dist/hooks/post-tool-telemetry.js +71 -0
- package/dist/hooks/prompt-router.js +1 -1
- package/dist/hooks/session-init.js +14 -1
- package/dist/manifests/claude-hooks.json +12 -0
- package/dist/manifests/opencode-manifest.json +10 -0
- package/dist/manifests/portability-report.json +6 -18
- package/dist/scripts/build-agents.d.ts +6 -0
- package/dist/scripts/build-agents.d.ts.map +1 -1
- package/dist/scripts/build-agents.js +17 -0
- package/dist/scripts/build-agents.js.map +1 -1
- package/dist/scripts/build-hooks.d.ts.map +1 -1
- package/dist/scripts/build-hooks.js +7 -0
- package/dist/scripts/build-hooks.js.map +1 -1
- package/dist/scripts/smoke/smoke-consumer.js +153 -3
- package/dist/scripts/smoke/smoke-consumer.js.map +1 -1
- package/dist/src/shared/paths.d.ts +3 -1
- package/dist/src/shared/paths.d.ts.map +1 -1
- package/dist/src/shared/paths.js +38 -2
- package/dist/src/shared/paths.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/shared/json-store.js
|
|
2
|
+
import { constants as fsConstants, appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
var inProcessQueues = new Map;
|
|
5
|
+
var APPEND_SIZE_WARN_THRESHOLD = 4 * 1024;
|
|
6
|
+
function appendJsonLine(filePath, record) {
|
|
7
|
+
const line = JSON.stringify(record) + `
|
|
8
|
+
`;
|
|
9
|
+
if (line.length > APPEND_SIZE_WARN_THRESHOLD) {
|
|
10
|
+
console.error(`[json-store] appendJsonLine line exceeds ${APPEND_SIZE_WARN_THRESHOLD} bytes ` + `(${line.length}) — write may not be atomic on some filesystems. path=${filePath}`);
|
|
11
|
+
}
|
|
12
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
appendFileSync(filePath, line);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// assets/hooks/post-tool-telemetry/handler.ts
|
|
17
|
+
import { join, resolve, relative } from "node:path";
|
|
18
|
+
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "ApplyPatch", "NotebookEdit"]);
|
|
19
|
+
function isWithinMemory(filePath, projectRoot) {
|
|
20
|
+
const memRoot = resolve(projectRoot, ".nexus/memory");
|
|
21
|
+
const abs = resolve(filePath);
|
|
22
|
+
return abs.startsWith(memRoot + "/") || abs === memRoot;
|
|
23
|
+
}
|
|
24
|
+
var handler = async (input) => {
|
|
25
|
+
if (input.hook_event_name !== "PostToolUse")
|
|
26
|
+
return;
|
|
27
|
+
const { cwd, session_id, tool_name, agent_id } = input;
|
|
28
|
+
const toolInput = input.tool_input ?? {};
|
|
29
|
+
if (tool_name === "Read") {
|
|
30
|
+
const filePath = toolInput.file_path;
|
|
31
|
+
if (filePath && isWithinMemory(filePath, cwd)) {
|
|
32
|
+
appendJsonLine(join(cwd, ".nexus/memory-access.jsonl"), {
|
|
33
|
+
path: relative(cwd, resolve(filePath)),
|
|
34
|
+
accessed_at: new Date().toISOString(),
|
|
35
|
+
agent: agent_id ?? null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (EDIT_TOOLS.has(tool_name) && agent_id) {
|
|
40
|
+
const filePath = toolInput.file_path ?? toolInput.notebook_path;
|
|
41
|
+
if (filePath) {
|
|
42
|
+
appendJsonLine(join(cwd, ".nexus/state", session_id, "tool-log.jsonl"), {
|
|
43
|
+
ts: new Date().toISOString(),
|
|
44
|
+
agent_id,
|
|
45
|
+
tool: tool_name,
|
|
46
|
+
file: relative(cwd, resolve(filePath)),
|
|
47
|
+
status: "ok"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var handler_default = handler;
|
|
53
|
+
|
|
54
|
+
// ../../../../../tmp/nexus-hook-entry-post-tool-telemetry-1776690665643/post-tool-telemetry-entry.ts
|
|
55
|
+
import { readFileSync } from "node:fs";
|
|
56
|
+
async function main() {
|
|
57
|
+
let raw = "";
|
|
58
|
+
try {
|
|
59
|
+
raw = readFileSync(0, "utf-8");
|
|
60
|
+
} catch {}
|
|
61
|
+
const input = raw ? JSON.parse(raw) : {};
|
|
62
|
+
const result = await handler_default(input);
|
|
63
|
+
if (result != null && result !== undefined) {
|
|
64
|
+
process.stdout.write(JSON.stringify(result));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
main().then(() => process.exit(0), (err) => {
|
|
68
|
+
process.stderr.write(String(err?.stack ?? err) + `
|
|
69
|
+
`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
@@ -7304,7 +7304,7 @@ var handler = async (input) => {
|
|
|
7304
7304
|
};
|
|
7305
7305
|
var handler_default = handler;
|
|
7306
7306
|
|
|
7307
|
-
// ../../../../../tmp/nexus-hook-entry-prompt-router-
|
|
7307
|
+
// ../../../../../tmp/nexus-hook-entry-prompt-router-1776690665662/prompt-router-entry.ts
|
|
7308
7308
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
7309
7309
|
globalThis.__NEXUS_INLINE_INVOCATIONS__ = { subagent_spawn: { args: ["target_role", "prompt", "name"], templates: { claude: 'Agent({ subagent_type: "{target_role}", prompt: "{prompt}", description: "{name}" })', opencode: 'task({ subagent_type: "{target_role}", prompt: "{prompt}", description: "{name}" })', codex: 'spawn_agent("{target_role}", "{prompt}")' }, notes: { claude: `description field is optional; omit when name arg is absent. model field may be added to override the spawned agent's model.
|
|
7310
7310
|
`, opencode: `description is required by OpenCode's Zod schema — use target_role as fallback when name is absent. task_id param enables session resume (§15).
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// assets/hooks/session-init/handler.ts
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/shared/paths.ts
|
|
6
|
+
function getParentPid() {
|
|
7
|
+
const testOverride = parseInt(process.env["NEXUS_TEST_PPID"] ?? "");
|
|
8
|
+
return testOverride || process.ppid;
|
|
9
|
+
}
|
|
10
|
+
var byPpidCache = new Map;
|
|
11
|
+
|
|
12
|
+
// assets/hooks/session-init/handler.ts
|
|
4
13
|
var handler = async (input) => {
|
|
5
14
|
if (input.hook_event_name !== "SessionStart")
|
|
6
15
|
return;
|
|
@@ -14,10 +23,14 @@ var handler = async (input) => {
|
|
|
14
23
|
mkdirSync(sessionDir, { recursive: true });
|
|
15
24
|
writeFileSync(join(sessionDir, "agent-tracker.json"), "[]");
|
|
16
25
|
writeFileSync(join(sessionDir, "tool-log.jsonl"), "");
|
|
26
|
+
const ppid = getParentPid();
|
|
27
|
+
const byPpidDir = join(input.cwd, ".nexus/state/runtime/by-ppid");
|
|
28
|
+
mkdirSync(byPpidDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(byPpidDir, `${ppid}.json`), JSON.stringify({ session_id: input.session_id, updated_at: new Date().toISOString(), cwd: input.cwd }));
|
|
17
30
|
};
|
|
18
31
|
var handler_default = handler;
|
|
19
32
|
|
|
20
|
-
// ../../../../../tmp/nexus-hook-entry-session-init-
|
|
33
|
+
// ../../../../../tmp/nexus-hook-entry-session-init-1776690665653/session-init-entry.ts
|
|
21
34
|
import { readFileSync } from "node:fs";
|
|
22
35
|
async function main() {
|
|
23
36
|
let raw = "";
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Read|Edit|Write|MultiEdit|NotebookEdit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/hooks/post-tool-telemetry.js",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
3
15
|
"SessionStart": [
|
|
4
16
|
{
|
|
5
17
|
"matcher": "*",
|
|
@@ -1,21 +1,124 @@
|
|
|
1
|
+
// src/shared/json-store.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, appendFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
var inProcessQueues = new Map;
|
|
7
|
+
async function runWithInProcessLock(filePath, action) {
|
|
8
|
+
const previous = inProcessQueues.get(filePath) ?? Promise.resolve();
|
|
9
|
+
let release = () => {};
|
|
10
|
+
const gate = new Promise((resolve) => {
|
|
11
|
+
release = resolve;
|
|
12
|
+
});
|
|
13
|
+
const entry = previous.then(() => gate);
|
|
14
|
+
inProcessQueues.set(filePath, entry);
|
|
15
|
+
await previous;
|
|
16
|
+
try {
|
|
17
|
+
return await action();
|
|
18
|
+
} finally {
|
|
19
|
+
release();
|
|
20
|
+
entry.finally(() => {
|
|
21
|
+
if (inProcessQueues.get(filePath) === entry) {
|
|
22
|
+
inProcessQueues.delete(filePath);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var LOCK_RETRY_INTERVAL_MS = 100;
|
|
28
|
+
var LOCK_MAX_RETRIES = 50;
|
|
29
|
+
var LOCK_STALE_MS = 30000;
|
|
30
|
+
function lockPath(filePath) {
|
|
31
|
+
return `${filePath}.lock`;
|
|
32
|
+
}
|
|
33
|
+
async function acquireFsLock(filePath) {
|
|
34
|
+
const lp = lockPath(filePath);
|
|
35
|
+
for (let attempt = 0;attempt <= LOCK_MAX_RETRIES; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
const fd = await fs.open(lp, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL);
|
|
38
|
+
await fd.close();
|
|
39
|
+
return;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const e = err;
|
|
42
|
+
if (e.code !== "EEXIST")
|
|
43
|
+
throw err;
|
|
44
|
+
try {
|
|
45
|
+
const stat = await fs.stat(lp);
|
|
46
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
47
|
+
if (ageMs > LOCK_STALE_MS) {
|
|
48
|
+
await fs.unlink(lp).catch(() => {
|
|
49
|
+
return;
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (attempt === LOCK_MAX_RETRIES) {
|
|
57
|
+
throw new Error(`Failed to acquire lock for "${filePath}" after ${LOCK_MAX_RETRIES} retries`);
|
|
58
|
+
}
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function releaseFsLock(filePath) {
|
|
64
|
+
await fs.unlink(lockPath(filePath)).catch(() => {
|
|
65
|
+
return;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function readJsonFile(filePath, defaultValue) {
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const e = err;
|
|
74
|
+
if (e.code === "ENOENT")
|
|
75
|
+
return defaultValue;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return defaultValue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function writeJsonFile(filePath, data) {
|
|
85
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
86
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
|
|
87
|
+
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2) + `
|
|
88
|
+
`, "utf8");
|
|
89
|
+
await fs.rename(tmpPath, filePath);
|
|
90
|
+
}
|
|
91
|
+
async function updateJsonFileLocked(filePath, defaultValue, updater) {
|
|
92
|
+
return runWithInProcessLock(filePath, async () => {
|
|
93
|
+
await acquireFsLock(filePath);
|
|
94
|
+
try {
|
|
95
|
+
const current = await readJsonFile(filePath, defaultValue);
|
|
96
|
+
const next = await updater(current);
|
|
97
|
+
await writeJsonFile(filePath, next);
|
|
98
|
+
return next;
|
|
99
|
+
} finally {
|
|
100
|
+
await releaseFsLock(filePath);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
var APPEND_SIZE_WARN_THRESHOLD = 4 * 1024;
|
|
105
|
+
|
|
1
106
|
// assets/hooks/agent-bootstrap/handler.ts
|
|
2
107
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
108
|
import { join } from "node:path";
|
|
4
109
|
var CORE_INDEX_SIZE_LIMIT = 2 * 1024;
|
|
5
110
|
function loadValidRoles(cwd) {
|
|
111
|
+
const inlined = globalThis.__NEXUS_INLINE_AGENT_ROLES__;
|
|
112
|
+
if (Array.isArray(inlined))
|
|
113
|
+
return inlined;
|
|
6
114
|
const agentsDir = join(cwd, "assets/agents");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (entry.isDirectory())
|
|
11
|
-
roles.push(entry.name);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return roles;
|
|
115
|
+
if (!existsSync(agentsDir))
|
|
116
|
+
return [];
|
|
117
|
+
return readdirSync(agentsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
15
118
|
}
|
|
16
|
-
function readFirstLine(
|
|
119
|
+
function readFirstLine(path2) {
|
|
17
120
|
try {
|
|
18
|
-
const content = readFileSync(
|
|
121
|
+
const content = readFileSync(path2, "utf-8");
|
|
19
122
|
const firstNonEmpty = content.split(`
|
|
20
123
|
`).find((l) => l.trim().length > 0) ?? "";
|
|
21
124
|
return firstNonEmpty.replace(/^#+\s*/, "").slice(0, 80);
|
|
@@ -76,6 +179,19 @@ var handler = async (input) => {
|
|
|
76
179
|
const validRoles = loadValidRoles(cwd);
|
|
77
180
|
if (!validRoles.includes(agent_type))
|
|
78
181
|
return;
|
|
182
|
+
const trackerPath = join(cwd, ".nexus/state", session_id, "agent-tracker.json");
|
|
183
|
+
await updateJsonFileLocked(trackerPath, [], (tracker) => {
|
|
184
|
+
const list = Array.isArray(tracker) ? tracker : [];
|
|
185
|
+
if (list.find((e) => e["agent_id"] === agent_id))
|
|
186
|
+
return list;
|
|
187
|
+
list.push({
|
|
188
|
+
agent_id,
|
|
189
|
+
agent_type,
|
|
190
|
+
started_at: new Date().toISOString(),
|
|
191
|
+
status: "running"
|
|
192
|
+
});
|
|
193
|
+
return list;
|
|
194
|
+
});
|
|
79
195
|
const parts = [];
|
|
80
196
|
const coreIndex = buildCoreIndex(cwd);
|
|
81
197
|
if (coreIndex) {
|
|
@@ -101,8 +217,9 @@ ${ruleContent}
|
|
|
101
217
|
};
|
|
102
218
|
var handler_default = handler;
|
|
103
219
|
|
|
104
|
-
// ../../../../../tmp/nexus-hook-entry-agent-bootstrap-
|
|
220
|
+
// ../../../../../tmp/nexus-hook-entry-agent-bootstrap-1776690665703/agent-bootstrap-entry.ts
|
|
105
221
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
222
|
+
globalThis.__NEXUS_INLINE_AGENT_ROLES__ = ["architect", "designer", "engineer", "reviewer", "strategist", "researcher", "postdoc", "lead", "tester", "writer"];
|
|
106
223
|
async function main() {
|
|
107
224
|
let raw = "";
|
|
108
225
|
try {
|
|
@@ -160,7 +160,7 @@ Subagent "${agent_type}" finished. Tasks still pending with this role: ${ids}. R
|
|
|
160
160
|
};
|
|
161
161
|
var handler_default = handler;
|
|
162
162
|
|
|
163
|
-
// ../../../../../tmp/nexus-hook-entry-agent-finalize-
|
|
163
|
+
// ../../../../../tmp/nexus-hook-entry-agent-finalize-1776690665695/agent-finalize-entry.ts
|
|
164
164
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
165
165
|
async function main() {
|
|
166
166
|
let raw = "";
|
|
@@ -7304,7 +7304,7 @@ var handler = async (input) => {
|
|
|
7304
7304
|
};
|
|
7305
7305
|
var handler_default = handler;
|
|
7306
7306
|
|
|
7307
|
-
// ../../../../../tmp/nexus-hook-entry-prompt-router-
|
|
7307
|
+
// ../../../../../tmp/nexus-hook-entry-prompt-router-1776690665662/prompt-router-entry.ts
|
|
7308
7308
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
7309
7309
|
globalThis.__NEXUS_INLINE_INVOCATIONS__ = { subagent_spawn: { args: ["target_role", "prompt", "name"], templates: { claude: 'Agent({ subagent_type: "{target_role}", prompt: "{prompt}", description: "{name}" })', opencode: 'task({ subagent_type: "{target_role}", prompt: "{prompt}", description: "{name}" })', codex: 'spawn_agent("{target_role}", "{prompt}")' }, notes: { claude: `description field is optional; omit when name arg is absent. model field may be added to override the spawned agent's model.
|
|
7310
7310
|
`, opencode: `description is required by OpenCode's Zod schema — use target_role as fallback when name is absent. task_id param enables session resume (§15).
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// assets/hooks/session-init/handler.ts
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/shared/paths.ts
|
|
6
|
+
function getParentPid() {
|
|
7
|
+
const testOverride = parseInt(process.env["NEXUS_TEST_PPID"] ?? "");
|
|
8
|
+
return testOverride || process.ppid;
|
|
9
|
+
}
|
|
10
|
+
var byPpidCache = new Map;
|
|
11
|
+
|
|
12
|
+
// assets/hooks/session-init/handler.ts
|
|
4
13
|
var handler = async (input) => {
|
|
5
14
|
if (input.hook_event_name !== "SessionStart")
|
|
6
15
|
return;
|
|
@@ -14,10 +23,14 @@ var handler = async (input) => {
|
|
|
14
23
|
mkdirSync(sessionDir, { recursive: true });
|
|
15
24
|
writeFileSync(join(sessionDir, "agent-tracker.json"), "[]");
|
|
16
25
|
writeFileSync(join(sessionDir, "tool-log.jsonl"), "");
|
|
26
|
+
const ppid = getParentPid();
|
|
27
|
+
const byPpidDir = join(input.cwd, ".nexus/state/runtime/by-ppid");
|
|
28
|
+
mkdirSync(byPpidDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(byPpidDir, `${ppid}.json`), JSON.stringify({ session_id: input.session_id, updated_at: new Date().toISOString(), cwd: input.cwd }));
|
|
17
30
|
};
|
|
18
31
|
var handler_default = handler;
|
|
19
32
|
|
|
20
|
-
// ../../../../../tmp/nexus-hook-entry-session-init-
|
|
33
|
+
// ../../../../../tmp/nexus-hook-entry-session-init-1776690665653/session-init-entry.ts
|
|
21
34
|
import { readFileSync } from "node:fs";
|
|
22
35
|
async function main() {
|
|
23
36
|
let raw = "";
|
|
@@ -1,21 +1,124 @@
|
|
|
1
|
+
// src/shared/json-store.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, appendFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
var inProcessQueues = new Map;
|
|
7
|
+
async function runWithInProcessLock(filePath, action) {
|
|
8
|
+
const previous = inProcessQueues.get(filePath) ?? Promise.resolve();
|
|
9
|
+
let release = () => {};
|
|
10
|
+
const gate = new Promise((resolve) => {
|
|
11
|
+
release = resolve;
|
|
12
|
+
});
|
|
13
|
+
const entry = previous.then(() => gate);
|
|
14
|
+
inProcessQueues.set(filePath, entry);
|
|
15
|
+
await previous;
|
|
16
|
+
try {
|
|
17
|
+
return await action();
|
|
18
|
+
} finally {
|
|
19
|
+
release();
|
|
20
|
+
entry.finally(() => {
|
|
21
|
+
if (inProcessQueues.get(filePath) === entry) {
|
|
22
|
+
inProcessQueues.delete(filePath);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var LOCK_RETRY_INTERVAL_MS = 100;
|
|
28
|
+
var LOCK_MAX_RETRIES = 50;
|
|
29
|
+
var LOCK_STALE_MS = 30000;
|
|
30
|
+
function lockPath(filePath) {
|
|
31
|
+
return `${filePath}.lock`;
|
|
32
|
+
}
|
|
33
|
+
async function acquireFsLock(filePath) {
|
|
34
|
+
const lp = lockPath(filePath);
|
|
35
|
+
for (let attempt = 0;attempt <= LOCK_MAX_RETRIES; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
const fd = await fs.open(lp, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL);
|
|
38
|
+
await fd.close();
|
|
39
|
+
return;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const e = err;
|
|
42
|
+
if (e.code !== "EEXIST")
|
|
43
|
+
throw err;
|
|
44
|
+
try {
|
|
45
|
+
const stat = await fs.stat(lp);
|
|
46
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
47
|
+
if (ageMs > LOCK_STALE_MS) {
|
|
48
|
+
await fs.unlink(lp).catch(() => {
|
|
49
|
+
return;
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (attempt === LOCK_MAX_RETRIES) {
|
|
57
|
+
throw new Error(`Failed to acquire lock for "${filePath}" after ${LOCK_MAX_RETRIES} retries`);
|
|
58
|
+
}
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function releaseFsLock(filePath) {
|
|
64
|
+
await fs.unlink(lockPath(filePath)).catch(() => {
|
|
65
|
+
return;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function readJsonFile(filePath, defaultValue) {
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const e = err;
|
|
74
|
+
if (e.code === "ENOENT")
|
|
75
|
+
return defaultValue;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return defaultValue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function writeJsonFile(filePath, data) {
|
|
85
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
86
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
|
|
87
|
+
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2) + `
|
|
88
|
+
`, "utf8");
|
|
89
|
+
await fs.rename(tmpPath, filePath);
|
|
90
|
+
}
|
|
91
|
+
async function updateJsonFileLocked(filePath, defaultValue, updater) {
|
|
92
|
+
return runWithInProcessLock(filePath, async () => {
|
|
93
|
+
await acquireFsLock(filePath);
|
|
94
|
+
try {
|
|
95
|
+
const current = await readJsonFile(filePath, defaultValue);
|
|
96
|
+
const next = await updater(current);
|
|
97
|
+
await writeJsonFile(filePath, next);
|
|
98
|
+
return next;
|
|
99
|
+
} finally {
|
|
100
|
+
await releaseFsLock(filePath);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
var APPEND_SIZE_WARN_THRESHOLD = 4 * 1024;
|
|
105
|
+
|
|
1
106
|
// assets/hooks/agent-bootstrap/handler.ts
|
|
2
107
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
108
|
import { join } from "node:path";
|
|
4
109
|
var CORE_INDEX_SIZE_LIMIT = 2 * 1024;
|
|
5
110
|
function loadValidRoles(cwd) {
|
|
111
|
+
const inlined = globalThis.__NEXUS_INLINE_AGENT_ROLES__;
|
|
112
|
+
if (Array.isArray(inlined))
|
|
113
|
+
return inlined;
|
|
6
114
|
const agentsDir = join(cwd, "assets/agents");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (entry.isDirectory())
|
|
11
|
-
roles.push(entry.name);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return roles;
|
|
115
|
+
if (!existsSync(agentsDir))
|
|
116
|
+
return [];
|
|
117
|
+
return readdirSync(agentsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
15
118
|
}
|
|
16
|
-
function readFirstLine(
|
|
119
|
+
function readFirstLine(path2) {
|
|
17
120
|
try {
|
|
18
|
-
const content = readFileSync(
|
|
121
|
+
const content = readFileSync(path2, "utf-8");
|
|
19
122
|
const firstNonEmpty = content.split(`
|
|
20
123
|
`).find((l) => l.trim().length > 0) ?? "";
|
|
21
124
|
return firstNonEmpty.replace(/^#+\s*/, "").slice(0, 80);
|
|
@@ -76,6 +179,19 @@ var handler = async (input) => {
|
|
|
76
179
|
const validRoles = loadValidRoles(cwd);
|
|
77
180
|
if (!validRoles.includes(agent_type))
|
|
78
181
|
return;
|
|
182
|
+
const trackerPath = join(cwd, ".nexus/state", session_id, "agent-tracker.json");
|
|
183
|
+
await updateJsonFileLocked(trackerPath, [], (tracker) => {
|
|
184
|
+
const list = Array.isArray(tracker) ? tracker : [];
|
|
185
|
+
if (list.find((e) => e["agent_id"] === agent_id))
|
|
186
|
+
return list;
|
|
187
|
+
list.push({
|
|
188
|
+
agent_id,
|
|
189
|
+
agent_type,
|
|
190
|
+
started_at: new Date().toISOString(),
|
|
191
|
+
status: "running"
|
|
192
|
+
});
|
|
193
|
+
return list;
|
|
194
|
+
});
|
|
79
195
|
const parts = [];
|
|
80
196
|
const coreIndex = buildCoreIndex(cwd);
|
|
81
197
|
if (coreIndex) {
|
|
@@ -101,8 +217,9 @@ ${ruleContent}
|
|
|
101
217
|
};
|
|
102
218
|
var handler_default = handler;
|
|
103
219
|
|
|
104
|
-
// ../../../../../tmp/nexus-hook-entry-agent-bootstrap-
|
|
220
|
+
// ../../../../../tmp/nexus-hook-entry-agent-bootstrap-1776690665703/agent-bootstrap-entry.ts
|
|
105
221
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
222
|
+
globalThis.__NEXUS_INLINE_AGENT_ROLES__ = ["architect", "designer", "engineer", "reviewer", "strategist", "researcher", "postdoc", "lead", "tester", "writer"];
|
|
106
223
|
async function main() {
|
|
107
224
|
let raw = "";
|
|
108
225
|
try {
|
|
@@ -160,7 +160,7 @@ Subagent "${agent_type}" finished. Tasks still pending with this role: ${ids}. R
|
|
|
160
160
|
};
|
|
161
161
|
var handler_default = handler;
|
|
162
162
|
|
|
163
|
-
// ../../../../../tmp/nexus-hook-entry-agent-finalize-
|
|
163
|
+
// ../../../../../tmp/nexus-hook-entry-agent-finalize-1776690665695/agent-finalize-entry.ts
|
|
164
164
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
165
165
|
async function main() {
|
|
166
166
|
let raw = "";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/shared/json-store.js
|
|
2
|
+
import { constants as fsConstants, appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
var inProcessQueues = new Map;
|
|
5
|
+
var APPEND_SIZE_WARN_THRESHOLD = 4 * 1024;
|
|
6
|
+
function appendJsonLine(filePath, record) {
|
|
7
|
+
const line = JSON.stringify(record) + `
|
|
8
|
+
`;
|
|
9
|
+
if (line.length > APPEND_SIZE_WARN_THRESHOLD) {
|
|
10
|
+
console.error(`[json-store] appendJsonLine line exceeds ${APPEND_SIZE_WARN_THRESHOLD} bytes ` + `(${line.length}) — write may not be atomic on some filesystems. path=${filePath}`);
|
|
11
|
+
}
|
|
12
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
appendFileSync(filePath, line);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// assets/hooks/post-tool-telemetry/handler.ts
|
|
17
|
+
import { join, resolve, relative } from "node:path";
|
|
18
|
+
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "ApplyPatch", "NotebookEdit"]);
|
|
19
|
+
function isWithinMemory(filePath, projectRoot) {
|
|
20
|
+
const memRoot = resolve(projectRoot, ".nexus/memory");
|
|
21
|
+
const abs = resolve(filePath);
|
|
22
|
+
return abs.startsWith(memRoot + "/") || abs === memRoot;
|
|
23
|
+
}
|
|
24
|
+
var handler = async (input) => {
|
|
25
|
+
if (input.hook_event_name !== "PostToolUse")
|
|
26
|
+
return;
|
|
27
|
+
const { cwd, session_id, tool_name, agent_id } = input;
|
|
28
|
+
const toolInput = input.tool_input ?? {};
|
|
29
|
+
if (tool_name === "Read") {
|
|
30
|
+
const filePath = toolInput.file_path;
|
|
31
|
+
if (filePath && isWithinMemory(filePath, cwd)) {
|
|
32
|
+
appendJsonLine(join(cwd, ".nexus/memory-access.jsonl"), {
|
|
33
|
+
path: relative(cwd, resolve(filePath)),
|
|
34
|
+
accessed_at: new Date().toISOString(),
|
|
35
|
+
agent: agent_id ?? null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (EDIT_TOOLS.has(tool_name) && agent_id) {
|
|
40
|
+
const filePath = toolInput.file_path ?? toolInput.notebook_path;
|
|
41
|
+
if (filePath) {
|
|
42
|
+
appendJsonLine(join(cwd, ".nexus/state", session_id, "tool-log.jsonl"), {
|
|
43
|
+
ts: new Date().toISOString(),
|
|
44
|
+
agent_id,
|
|
45
|
+
tool: tool_name,
|
|
46
|
+
file: relative(cwd, resolve(filePath)),
|
|
47
|
+
status: "ok"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var handler_default = handler;
|
|
53
|
+
|
|
54
|
+
// ../../../../../tmp/nexus-hook-entry-post-tool-telemetry-1776690665643/post-tool-telemetry-entry.ts
|
|
55
|
+
import { readFileSync } from "node:fs";
|
|
56
|
+
async function main() {
|
|
57
|
+
let raw = "";
|
|
58
|
+
try {
|
|
59
|
+
raw = readFileSync(0, "utf-8");
|
|
60
|
+
} catch {}
|
|
61
|
+
const input = raw ? JSON.parse(raw) : {};
|
|
62
|
+
const result = await handler_default(input);
|
|
63
|
+
if (result != null && result !== undefined) {
|
|
64
|
+
process.stdout.write(JSON.stringify(result));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
main().then(() => process.exit(0), (err) => {
|
|
68
|
+
process.stderr.write(String(err?.stack ?? err) + `
|
|
69
|
+
`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
@@ -7304,7 +7304,7 @@ var handler = async (input) => {
|
|
|
7304
7304
|
};
|
|
7305
7305
|
var handler_default = handler;
|
|
7306
7306
|
|
|
7307
|
-
// ../../../../../tmp/nexus-hook-entry-prompt-router-
|
|
7307
|
+
// ../../../../../tmp/nexus-hook-entry-prompt-router-1776690665662/prompt-router-entry.ts
|
|
7308
7308
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
7309
7309
|
globalThis.__NEXUS_INLINE_INVOCATIONS__ = { subagent_spawn: { args: ["target_role", "prompt", "name"], templates: { claude: 'Agent({ subagent_type: "{target_role}", prompt: "{prompt}", description: "{name}" })', opencode: 'task({ subagent_type: "{target_role}", prompt: "{prompt}", description: "{name}" })', codex: 'spawn_agent("{target_role}", "{prompt}")' }, notes: { claude: `description field is optional; omit when name arg is absent. model field may be added to override the spawned agent's model.
|
|
7310
7310
|
`, opencode: `description is required by OpenCode's Zod schema — use target_role as fallback when name is absent. task_id param enables session resume (§15).
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// assets/hooks/session-init/handler.ts
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/shared/paths.ts
|
|
6
|
+
function getParentPid() {
|
|
7
|
+
const testOverride = parseInt(process.env["NEXUS_TEST_PPID"] ?? "");
|
|
8
|
+
return testOverride || process.ppid;
|
|
9
|
+
}
|
|
10
|
+
var byPpidCache = new Map;
|
|
11
|
+
|
|
12
|
+
// assets/hooks/session-init/handler.ts
|
|
4
13
|
var handler = async (input) => {
|
|
5
14
|
if (input.hook_event_name !== "SessionStart")
|
|
6
15
|
return;
|
|
@@ -14,10 +23,14 @@ var handler = async (input) => {
|
|
|
14
23
|
mkdirSync(sessionDir, { recursive: true });
|
|
15
24
|
writeFileSync(join(sessionDir, "agent-tracker.json"), "[]");
|
|
16
25
|
writeFileSync(join(sessionDir, "tool-log.jsonl"), "");
|
|
26
|
+
const ppid = getParentPid();
|
|
27
|
+
const byPpidDir = join(input.cwd, ".nexus/state/runtime/by-ppid");
|
|
28
|
+
mkdirSync(byPpidDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(byPpidDir, `${ppid}.json`), JSON.stringify({ session_id: input.session_id, updated_at: new Date().toISOString(), cwd: input.cwd }));
|
|
17
30
|
};
|
|
18
31
|
var handler_default = handler;
|
|
19
32
|
|
|
20
|
-
// ../../../../../tmp/nexus-hook-entry-session-init-
|
|
33
|
+
// ../../../../../tmp/nexus-hook-entry-session-init-1776690665653/session-init-entry.ts
|
|
21
34
|
import { readFileSync } from "node:fs";
|
|
22
35
|
async function main() {
|
|
23
36
|
let raw = "";
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Read|Edit|Write|MultiEdit|NotebookEdit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/hooks/post-tool-telemetry.js",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
3
15
|
"SessionStart": [
|
|
4
16
|
{
|
|
5
17
|
"matcher": "*",
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": [
|
|
3
|
+
{
|
|
4
|
+
"name": "post-tool-telemetry",
|
|
5
|
+
"events": [
|
|
6
|
+
"PostToolUse"
|
|
7
|
+
],
|
|
8
|
+
"matcher": "Read|Edit|Write|MultiEdit|ApplyPatch|NotebookEdit",
|
|
9
|
+
"handlerPath": "../hooks/post-tool-telemetry.js",
|
|
10
|
+
"priority": 10,
|
|
11
|
+
"timeout": 5
|
|
12
|
+
},
|
|
3
13
|
{
|
|
4
14
|
"name": "session-init",
|
|
5
15
|
"events": [
|