@moreih29/nexus-core 0.16.1 → 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.
Files changed (63) hide show
  1. package/assets/capability-matrix.yml +7 -5
  2. package/assets/hooks/agent-bootstrap/handler.test.ts +18 -17
  3. package/assets/hooks/agent-bootstrap/handler.ts +20 -7
  4. package/assets/hooks/post-tool-telemetry/meta.yml +1 -2
  5. package/assets/hooks/session-init/handler.ts +8 -7
  6. package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -1
  7. package/dist/assets/hooks/agent-bootstrap/handler.js +22 -8
  8. package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -1
  9. package/dist/assets/hooks/session-init/handler.d.ts.map +1 -1
  10. package/dist/assets/hooks/session-init/handler.js +5 -6
  11. package/dist/assets/hooks/session-init/handler.js.map +1 -1
  12. package/dist/claude/agents/architect.md +1 -1
  13. package/dist/claude/agents/designer.md +1 -1
  14. package/dist/claude/agents/engineer.md +1 -1
  15. package/dist/claude/agents/lead.md +1 -1
  16. package/dist/claude/agents/postdoc.md +1 -1
  17. package/dist/claude/agents/researcher.md +1 -1
  18. package/dist/claude/agents/reviewer.md +1 -1
  19. package/dist/claude/agents/strategist.md +1 -1
  20. package/dist/claude/agents/tester.md +1 -1
  21. package/dist/claude/agents/writer.md +1 -1
  22. package/dist/claude/dist/hooks/agent-bootstrap.js +128 -11
  23. package/dist/claude/dist/hooks/agent-finalize.js +1 -1
  24. package/dist/claude/dist/hooks/post-tool-telemetry.js +71 -0
  25. package/dist/claude/dist/hooks/prompt-router.js +1 -1
  26. package/dist/claude/dist/hooks/session-init.js +14 -1
  27. package/dist/claude/hooks/hooks.json +12 -0
  28. package/dist/codex/agents/architect.toml +3 -0
  29. package/dist/codex/agents/designer.toml +3 -0
  30. package/dist/codex/agents/engineer.toml +3 -0
  31. package/dist/codex/agents/postdoc.toml +3 -0
  32. package/dist/codex/agents/researcher.toml +3 -0
  33. package/dist/codex/agents/reviewer.toml +3 -0
  34. package/dist/codex/agents/strategist.toml +3 -0
  35. package/dist/codex/agents/tester.toml +3 -0
  36. package/dist/codex/agents/writer.toml +3 -0
  37. package/dist/codex/dist/hooks/agent-bootstrap.js +128 -11
  38. package/dist/codex/dist/hooks/agent-finalize.js +1 -1
  39. package/dist/codex/dist/hooks/prompt-router.js +1 -1
  40. package/dist/codex/dist/hooks/session-init.js +14 -1
  41. package/dist/hooks/agent-bootstrap.js +128 -11
  42. package/dist/hooks/agent-finalize.js +1 -1
  43. package/dist/hooks/post-tool-telemetry.js +71 -0
  44. package/dist/hooks/prompt-router.js +1 -1
  45. package/dist/hooks/session-init.js +14 -1
  46. package/dist/manifests/claude-hooks.json +12 -0
  47. package/dist/manifests/opencode-manifest.json +10 -0
  48. package/dist/manifests/portability-report.json +6 -18
  49. package/dist/scripts/build-agents.d.ts +6 -0
  50. package/dist/scripts/build-agents.d.ts.map +1 -1
  51. package/dist/scripts/build-agents.js +22 -1
  52. package/dist/scripts/build-agents.js.map +1 -1
  53. package/dist/scripts/build-hooks.d.ts.map +1 -1
  54. package/dist/scripts/build-hooks.js +7 -0
  55. package/dist/scripts/build-hooks.js.map +1 -1
  56. package/dist/scripts/smoke/smoke-consumer.js +153 -3
  57. package/dist/scripts/smoke/smoke-consumer.js.map +1 -1
  58. package/dist/src/shared/paths.d.ts +3 -1
  59. package/dist/src/shared/paths.d.ts.map +1 -1
  60. package/dist/src/shared/paths.js +38 -2
  61. package/dist/src/shared/paths.js.map +1 -1
  62. package/docs/contract/harness-io.md +7 -2
  63. 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-1776672660215/prompt-router-entry.ts
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-1776672660208/session-init-entry.ts
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": "*",
@@ -169,4 +169,7 @@ When escalating, include:
169
169
  """
170
170
  model = "gpt-5.4"
171
171
  sandbox_mode = "read-only"
172
+
173
+ [mcp_servers.nx]
174
+ command = "nexus-mcp"
172
175
  disabled_tools = ["nx_task_add", "nx_task_update"]
@@ -117,4 +117,7 @@ All claims about impossibility, infeasibility, or platform limitations MUST incl
117
117
  """
118
118
  model = "gpt-5.4"
119
119
  sandbox_mode = "read-only"
120
+
121
+ [mcp_servers.nx]
122
+ command = "nexus-mcp"
120
123
  disabled_tools = ["nx_task_add", "nx_task_update"]
@@ -99,4 +99,7 @@ These are included so Lead can update the Phase 5 (Document) manifest.
99
99
 
100
100
  """
101
101
  model = "gpt-5.3-codex"
102
+
103
+ [mcp_servers.nx]
104
+ command = "nexus-mcp"
102
105
  disabled_tools = ["nx_task_add"]
@@ -114,4 +114,7 @@ Do not guess or force a synthesis when the evidence does not support one. Escala
114
114
  """
115
115
  model = "gpt-5.4"
116
116
  sandbox_mode = "read-only"
117
+
118
+ [mcp_servers.nx]
119
+ command = "nexus-mcp"
117
120
  disabled_tools = ["nx_task_add", "nx_task_update"]
@@ -130,4 +130,7 @@ Format for memory entries: include the research question, key findings, source U
130
130
  """
131
131
  model = "gpt-5.3-codex"
132
132
  sandbox_mode = "read-only"
133
+
134
+ [mcp_servers.nx]
135
+ command = "nexus-mcp"
133
136
  disabled_tools = ["nx_task_add"]
@@ -131,4 +131,7 @@ When writing a review report, use `nx_artifact_write` (filename, content) to sav
131
131
  """
132
132
  model = "gpt-5.3-codex"
133
133
  sandbox_mode = "read-only"
134
+
135
+ [mcp_servers.nx]
136
+ command = "nexus-mcp"
134
137
  disabled_tools = ["nx_task_add"]
@@ -108,4 +108,7 @@ When escalating, state: what you were asked, what you found, what is blocking yo
108
108
  """
109
109
  model = "gpt-5.4"
110
110
  sandbox_mode = "read-only"
111
+
112
+ [mcp_servers.nx]
113
+ command = "nexus-mcp"
111
114
  disabled_tools = ["nx_task_add", "nx_task_update"]
@@ -188,4 +188,7 @@ When writing verification reports or other deliverables to a file, use `nx_artif
188
188
  """
189
189
  model = "gpt-5.3-codex"
190
190
  sandbox_mode = "read-only"
191
+
192
+ [mcp_servers.nx]
193
+ command = "nexus-mcp"
191
194
  disabled_tools = ["nx_task_add"]
@@ -115,4 +115,7 @@ Do not escalate for minor phrasing ambiguity or formatting choices — those are
115
115
 
116
116
  """
117
117
  model = "gpt-5.3-codex"
118
+
119
+ [mcp_servers.nx]
120
+ command = "nexus-mcp"
118
121
  disabled_tools = ["nx_task_add"]
@@ -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
- const roles = [];
8
- if (existsSync(agentsDir)) {
9
- for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
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(path) {
119
+ function readFirstLine(path2) {
17
120
  try {
18
- const content = readFileSync(path, "utf-8");
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-1776672660252/agent-bootstrap-entry.ts
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-1776672660245/agent-finalize-entry.ts
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-1776672660215/prompt-router-entry.ts
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-1776672660208/session-init-entry.ts
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
- const roles = [];
8
- if (existsSync(agentsDir)) {
9
- for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
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(path) {
119
+ function readFirstLine(path2) {
17
120
  try {
18
- const content = readFileSync(path, "utf-8");
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-1776672660252/agent-bootstrap-entry.ts
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-1776672660245/agent-finalize-entry.ts
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 = "";