nexus-prime 6.3.0 → 6.4.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 (67) hide show
  1. package/dist/agents/adapters/mcp/async-gate.d.ts +96 -0
  2. package/dist/agents/adapters/mcp/async-gate.js +193 -0
  3. package/dist/agents/adapters/mcp/circuit.d.ts +40 -0
  4. package/dist/agents/adapters/mcp/circuit.js +135 -0
  5. package/dist/agents/adapters/mcp/definitions.js +102 -0
  6. package/dist/agents/adapters/mcp/dispatch.js +124 -11
  7. package/dist/agents/adapters/mcp/handlers/governance.js +17 -1
  8. package/dist/agents/adapters/mcp/handlers/memory.js +32 -4
  9. package/dist/agents/adapters/mcp/handlers/orchestration.js +36 -2
  10. package/dist/agents/adapters/mcp/handlers/runtime.js +37 -0
  11. package/dist/agents/adapters/mcp/handlers/workforce.d.ts +25 -0
  12. package/dist/agents/adapters/mcp/handlers/workforce.js +170 -0
  13. package/dist/agents/adapters/mcp/watchdog.d.ts +14 -0
  14. package/dist/agents/adapters/mcp/watchdog.js +70 -0
  15. package/dist/agents/adapters/mcp.js +10 -0
  16. package/dist/dashboard/app/index.html +6 -0
  17. package/dist/dashboard/app/main.js +19 -2
  18. package/dist/dashboard/app/styles/shell.css +15 -6
  19. package/dist/dashboard/app/styles/workforce.css +77 -0
  20. package/dist/dashboard/app/views/workforce.js +116 -1
  21. package/dist/dashboard/routes/governance.js +55 -0
  22. package/dist/dashboard/routes/workforce.d.ts +9 -0
  23. package/dist/dashboard/routes/workforce.js +75 -0
  24. package/dist/dashboard/server.js +23 -0
  25. package/dist/engines/event-bus.d.ts +14 -1
  26. package/dist/engines/memory-consolidator.d.ts +40 -0
  27. package/dist/engines/memory-consolidator.js +157 -0
  28. package/dist/engines/orchestrator/store.d.ts +44 -0
  29. package/dist/engines/orchestrator/store.js +153 -0
  30. package/dist/engines/orchestrator/types.d.ts +1 -0
  31. package/dist/engines/orchestrator.js +19 -11
  32. package/dist/engines/providers/index.d.ts +5 -0
  33. package/dist/engines/providers/index.js +4 -0
  34. package/dist/engines/providers/registry.d.ts +45 -0
  35. package/dist/engines/providers/registry.js +108 -0
  36. package/dist/engines/providers/types.d.ts +32 -0
  37. package/dist/engines/providers/types.js +5 -0
  38. package/dist/engines/token-supremacy.js +13 -22
  39. package/dist/engines/workspace-resolver.d.ts +3 -3
  40. package/dist/engines/workspace-resolver.js +3 -3
  41. package/dist/index.d.ts +10 -2
  42. package/dist/index.js +41 -1
  43. package/dist/nxl/codec.d.ts +22 -0
  44. package/dist/nxl/codec.js +199 -0
  45. package/dist/nxl/index.d.ts +12 -0
  46. package/dist/nxl/index.js +10 -0
  47. package/dist/phantom/runtime.js +7 -4
  48. package/dist/synapse/db/schema.js +3 -0
  49. package/dist/workforce/claim-lock.d.ts +16 -0
  50. package/dist/workforce/claim-lock.js +86 -0
  51. package/dist/workforce/dag.d.ts +20 -0
  52. package/dist/workforce/dag.js +90 -0
  53. package/dist/workforce/db/client.d.ts +22 -0
  54. package/dist/workforce/db/client.js +69 -0
  55. package/dist/workforce/db/schema.d.ts +9 -0
  56. package/dist/workforce/db/schema.js +115 -0
  57. package/dist/workforce/index.d.ts +54 -0
  58. package/dist/workforce/index.js +71 -0
  59. package/dist/workforce/jobs.d.ts +19 -0
  60. package/dist/workforce/jobs.js +128 -0
  61. package/dist/workforce/kanban-view.d.ts +7 -0
  62. package/dist/workforce/kanban-view.js +45 -0
  63. package/dist/workforce/types.d.ts +89 -0
  64. package/dist/workforce/types.js +9 -0
  65. package/dist/workforce/workers.d.ts +14 -0
  66. package/dist/workforce/workers.js +89 -0
  67. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * AsyncGate — lightweight async MCP handler wrapper.
3
+ *
4
+ * Long-running MCP tool calls timeout in Claude Code (~60s). This module lets
5
+ * any handler opt into an async contract: if work exceeds maxSyncMs, return
6
+ * {queued:true, runId} immediately and finish in the background. Callers poll
7
+ * nexus_run_status(runId) for completion.
8
+ *
9
+ * Design:
10
+ * - In-memory ring buffer (cap 200 jobs). No SQLite dep — keep hot path fast.
11
+ * - withAsyncGate wraps a handler fn; uses Promise.race against a deadline.
12
+ * - getAsyncGate() returns the process-singleton instance.
13
+ */
14
+ export type AsyncJobStatus = 'pending' | 'running' | 'completed' | 'failed';
15
+ export interface AsyncJob {
16
+ runId: string;
17
+ tool: string;
18
+ status: AsyncJobStatus;
19
+ stage: string;
20
+ progress: number;
21
+ queuedAt: number;
22
+ startedAt?: number;
23
+ completedAt?: number;
24
+ result?: {
25
+ content: Array<{
26
+ type: string;
27
+ text: string;
28
+ }>;
29
+ };
30
+ error?: string;
31
+ etaMs?: number;
32
+ }
33
+ export interface AsyncGateOpts {
34
+ /** Tool name — for job metadata */
35
+ tool: string;
36
+ /** If handler resolves within this window, return inline. Default: 2000ms */
37
+ maxSyncMs?: number;
38
+ /** Human-readable hint for estimated duration */
39
+ etaMs?: number;
40
+ }
41
+ type HandlerFn = () => Promise<{
42
+ content: Array<{
43
+ type: string;
44
+ text: string;
45
+ }>;
46
+ }>;
47
+ declare class AsyncGate {
48
+ private jobs;
49
+ private insertOrder;
50
+ /** Run handler; if it takes >maxSyncMs, queue it and return runId immediately. */
51
+ run(fn: HandlerFn, opts: AsyncGateOpts): Promise<{
52
+ content: Array<{
53
+ type: string;
54
+ text: string;
55
+ }>;
56
+ } | {
57
+ queued: true;
58
+ runId: string;
59
+ etaMs?: number;
60
+ }>;
61
+ getJob(runId: string): AsyncJob | undefined;
62
+ /** Format job status as MCP text response */
63
+ formatStatus(runId: string): {
64
+ content: Array<{
65
+ type: string;
66
+ text: string;
67
+ }>;
68
+ };
69
+ updateStage(runId: string, stage: string, progress: number): void;
70
+ /** Return jobs that have been running longer than their stale threshold. */
71
+ getStaleJobs(now: number, defaultStaleMs: number, etaMultiplier: number): AsyncJob[];
72
+ /** Mark a running job as failed (watchdog reap). */
73
+ markFailed(runId: string, reason: string): void;
74
+ private enqueueJob;
75
+ }
76
+ export declare function getAsyncGate(): AsyncGate;
77
+ /**
78
+ * Wrap an MCP handler function in the async gate.
79
+ *
80
+ * Usage:
81
+ * return withAsyncGate(() => doSlowWork(), { tool: 'nexus_orchestrate', maxSyncMs: 2000, etaMs: 30_000 });
82
+ *
83
+ * If work finishes within maxSyncMs → inline result (transparent).
84
+ * If not → { queued: true, runId } immediately; work continues in bg.
85
+ */
86
+ export declare function withAsyncGate(fn: HandlerFn, opts: AsyncGateOpts): Promise<{
87
+ content: Array<{
88
+ type: string;
89
+ text: string;
90
+ }>;
91
+ } | {
92
+ queued: true;
93
+ runId: string;
94
+ etaMs?: number;
95
+ }>;
96
+ export {};
@@ -0,0 +1,193 @@
1
+ /**
2
+ * AsyncGate — lightweight async MCP handler wrapper.
3
+ *
4
+ * Long-running MCP tool calls timeout in Claude Code (~60s). This module lets
5
+ * any handler opt into an async contract: if work exceeds maxSyncMs, return
6
+ * {queued:true, runId} immediately and finish in the background. Callers poll
7
+ * nexus_run_status(runId) for completion.
8
+ *
9
+ * Design:
10
+ * - In-memory ring buffer (cap 200 jobs). No SQLite dep — keep hot path fast.
11
+ * - withAsyncGate wraps a handler fn; uses Promise.race against a deadline.
12
+ * - getAsyncGate() returns the process-singleton instance.
13
+ */
14
+ import { randomUUID } from 'crypto';
15
+ import { nexusEventBus } from '../../../engines/event-bus.js';
16
+ // ─── Ring buffer ──────────────────────────────────────────────────────────────
17
+ const RING_CAP = 200;
18
+ class AsyncGate {
19
+ jobs = new Map();
20
+ insertOrder = [];
21
+ /** Run handler; if it takes >maxSyncMs, queue it and return runId immediately. */
22
+ async run(fn, opts) {
23
+ const maxSyncMs = opts.maxSyncMs ?? 2000;
24
+ const runId = randomUUID();
25
+ const job = {
26
+ runId,
27
+ tool: opts.tool,
28
+ status: 'pending',
29
+ stage: 'queued',
30
+ progress: 0,
31
+ queuedAt: Date.now(),
32
+ etaMs: opts.etaMs,
33
+ };
34
+ this.enqueueJob(job);
35
+ // Race: handler vs deadline
36
+ let syncResolved = false;
37
+ const deadline = new Promise((resolve) => setTimeout(() => resolve(null), maxSyncMs));
38
+ const work = (async () => {
39
+ job.status = 'running';
40
+ job.stage = 'executing';
41
+ job.startedAt = Date.now();
42
+ try {
43
+ const result = await fn();
44
+ job.status = 'completed';
45
+ job.stage = 'done';
46
+ job.progress = 100;
47
+ job.result = result;
48
+ job.completedAt = Date.now();
49
+ nexusEventBus.emit('async_gate.completed', { runId, tool: opts.tool });
50
+ return result;
51
+ }
52
+ catch (err) {
53
+ job.status = 'failed';
54
+ job.stage = 'error';
55
+ job.error = err instanceof Error ? err.message : String(err);
56
+ job.completedAt = Date.now();
57
+ nexusEventBus.emit('async_gate.failed', { runId, tool: opts.tool, error: job.error });
58
+ // Do not rethrow — if deadline already won there is no downstream catcher;
59
+ // job state is updated so polls return the error correctly.
60
+ }
61
+ })();
62
+ // Suppress unhandled rejection: deadline may win while work is still in flight.
63
+ work.catch(() => { });
64
+ const winner = await Promise.race([
65
+ work.then((r) => { syncResolved = true; return r; }),
66
+ deadline,
67
+ ]);
68
+ if (syncResolved && winner !== null) {
69
+ // Completed within deadline — return inline
70
+ return winner;
71
+ }
72
+ // Still running — return async receipt
73
+ return { queued: true, runId, etaMs: opts.etaMs };
74
+ }
75
+ getJob(runId) {
76
+ return this.jobs.get(runId);
77
+ }
78
+ /** Format job status as MCP text response */
79
+ formatStatus(runId) {
80
+ const job = this.jobs.get(runId);
81
+ if (!job) {
82
+ return { content: [{ type: 'text', text: `❌ Async run not found: ${runId}` }] };
83
+ }
84
+ if (job.status === 'completed' && job.result) {
85
+ return job.result;
86
+ }
87
+ if (job.status === 'failed') {
88
+ return {
89
+ content: [{
90
+ type: 'text',
91
+ text: `❌ Run ${runId} failed\nTool: ${job.tool}\nError: ${job.error ?? 'unknown'}`,
92
+ }],
93
+ };
94
+ }
95
+ const elapsed = Date.now() - job.queuedAt;
96
+ const etaLine = job.etaMs ? ` ETA: ~${Math.ceil((job.etaMs - elapsed) / 1000)}s remaining` : '';
97
+ return {
98
+ content: [{
99
+ type: 'text',
100
+ text: [
101
+ `⏳ Run ${runId}`,
102
+ ` Tool: ${job.tool}`,
103
+ ` Status: ${job.status}`,
104
+ ` Stage: ${job.stage}`,
105
+ ` Progress: ${job.progress}%`,
106
+ ` Elapsed: ${Math.round(elapsed / 100) / 10}s`,
107
+ etaLine,
108
+ '',
109
+ 'Poll again with nexus_run_status(runId) in a few seconds.',
110
+ ].filter(l => l !== undefined).join('\n'),
111
+ }],
112
+ };
113
+ }
114
+ updateStage(runId, stage, progress) {
115
+ const job = this.jobs.get(runId);
116
+ if (job && job.status === 'running') {
117
+ job.stage = stage;
118
+ job.progress = Math.min(99, progress);
119
+ }
120
+ }
121
+ /** Return jobs that have been running longer than their stale threshold. */
122
+ getStaleJobs(now, defaultStaleMs, etaMultiplier) {
123
+ const stale = [];
124
+ for (const job of this.jobs.values()) {
125
+ if (job.status !== 'running')
126
+ continue;
127
+ const startedAt = job.startedAt ?? job.queuedAt;
128
+ const threshold = job.etaMs != null
129
+ ? job.etaMs * etaMultiplier
130
+ : defaultStaleMs;
131
+ if (now - startedAt > threshold)
132
+ stale.push(job);
133
+ }
134
+ return stale;
135
+ }
136
+ /** Mark a running job as failed (watchdog reap). */
137
+ markFailed(runId, reason) {
138
+ const job = this.jobs.get(runId);
139
+ if (job && (job.status === 'running' || job.status === 'pending')) {
140
+ job.status = 'failed';
141
+ job.stage = 'error';
142
+ job.error = reason;
143
+ job.completedAt = Date.now();
144
+ }
145
+ }
146
+ enqueueJob(job) {
147
+ if (this.insertOrder.length >= RING_CAP) {
148
+ // Skip eviction if oldest job is still running — losing it would make polls fail.
149
+ const oldestId = this.insertOrder[0];
150
+ const oldest = oldestId ? this.jobs.get(oldestId) : undefined;
151
+ if (oldest && (oldest.status === 'running' || oldest.status === 'pending')) {
152
+ // Buffer full with live jobs — evict second-oldest completed/failed instead
153
+ const evictIdx = this.insertOrder.findIndex(id => {
154
+ const j = this.jobs.get(id);
155
+ return j && j.status !== 'running' && j.status !== 'pending';
156
+ });
157
+ if (evictIdx !== -1) {
158
+ const evictId = this.insertOrder.splice(evictIdx, 1)[0];
159
+ if (evictId)
160
+ this.jobs.delete(evictId);
161
+ }
162
+ // If all live, just append without eviction (buffer temporarily oversized).
163
+ }
164
+ else {
165
+ this.insertOrder.shift();
166
+ if (oldestId)
167
+ this.jobs.delete(oldestId);
168
+ }
169
+ }
170
+ this.jobs.set(job.runId, job);
171
+ this.insertOrder.push(job.runId);
172
+ }
173
+ }
174
+ // ─── Singleton ────────────────────────────────────────────────────────────────
175
+ let _instance = null;
176
+ export function getAsyncGate() {
177
+ if (!_instance)
178
+ _instance = new AsyncGate();
179
+ return _instance;
180
+ }
181
+ // ─── withAsyncGate helper ─────────────────────────────────────────────────────
182
+ /**
183
+ * Wrap an MCP handler function in the async gate.
184
+ *
185
+ * Usage:
186
+ * return withAsyncGate(() => doSlowWork(), { tool: 'nexus_orchestrate', maxSyncMs: 2000, etaMs: 30_000 });
187
+ *
188
+ * If work finishes within maxSyncMs → inline result (transparent).
189
+ * If not → { queued: true, runId } immediately; work continues in bg.
190
+ */
191
+ export function withAsyncGate(fn, opts) {
192
+ return getAsyncGate().run(fn, opts);
193
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Circuit breaker + backpressure for MCP tool calls.
3
+ *
4
+ * Circuit breaker: track p99 latency over a rolling window per tool.
5
+ * If p99 exceeds CIRCUIT_TRIP_MS, tool is force-gated (async even for fast calls).
6
+ * Resets automatically after RESET_WINDOW_MS of clean calls.
7
+ *
8
+ * Backpressure: global in-flight counter. If above QUEUE_CAPACITY,
9
+ * return {busy: true, retryAfterMs} instead of queuing more work.
10
+ */
11
+ export type CircuitState = 'closed' | 'open' | 'half-open';
12
+ export interface CircuitStats {
13
+ tool: string;
14
+ state: CircuitState;
15
+ p99Ms: number;
16
+ sampleCount: number;
17
+ trippedAt?: number;
18
+ }
19
+ declare class CircuitManager {
20
+ private circuits;
21
+ private inFlight;
22
+ private getCircuit;
23
+ /** Check backpressure BEFORE enqueuing. Returns retryAfterMs or 0 if OK. */
24
+ checkCapacity(): number;
25
+ /** True if circuit is open (tool force-gated to async). */
26
+ isForceAsync(tool: string): boolean;
27
+ /** Record a completed call. Pass durationMs for p99 tracking. */
28
+ recordComplete(tool: string, durationMs: number, success: boolean): void;
29
+ /** Call when work starts (increments in-flight). */
30
+ recordStart(tool: string): void;
31
+ getStats(tool: string): CircuitStats;
32
+ getAllStats(): CircuitStats[];
33
+ getInFlight(): number;
34
+ }
35
+ export declare function getCircuitManager(): CircuitManager;
36
+ export declare function checkBackpressure(): {
37
+ ok: boolean;
38
+ retryAfterMs?: number;
39
+ };
40
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Circuit breaker + backpressure for MCP tool calls.
3
+ *
4
+ * Circuit breaker: track p99 latency over a rolling window per tool.
5
+ * If p99 exceeds CIRCUIT_TRIP_MS, tool is force-gated (async even for fast calls).
6
+ * Resets automatically after RESET_WINDOW_MS of clean calls.
7
+ *
8
+ * Backpressure: global in-flight counter. If above QUEUE_CAPACITY,
9
+ * return {busy: true, retryAfterMs} instead of queuing more work.
10
+ */
11
+ import { nexusEventBus } from '../../../engines/event-bus.js';
12
+ // ─── Config ───────────────────────────────────────────────────────────────────
13
+ const WINDOW_SIZE = 50; // rolling sample count per tool
14
+ const CIRCUIT_TRIP_MS = 20_000; // p99 > 20s → trip circuit
15
+ const RESET_WINDOW_MS = 60_000; // stay tripped for 60s before retry
16
+ const QUEUE_CAPACITY = 100; // max concurrent in-flight tool calls
17
+ // ─── CircuitBreaker ───────────────────────────────────────────────────────────
18
+ class ToolCircuit {
19
+ samples = [];
20
+ state = 'closed';
21
+ trippedAt = 0;
22
+ record(durationMs) {
23
+ this.samples.push(durationMs);
24
+ if (this.samples.length > WINDOW_SIZE)
25
+ this.samples.shift();
26
+ this.evaluate();
27
+ }
28
+ /** Returns true if this tool should be force-gated (async even for fast calls). */
29
+ isOpen() {
30
+ if (this.state === 'closed')
31
+ return false;
32
+ if (this.state === 'open') {
33
+ if (Date.now() - this.trippedAt >= RESET_WINDOW_MS) {
34
+ this.state = 'half-open';
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ // half-open: allow one attempt
40
+ return false;
41
+ }
42
+ onSuccess() {
43
+ if (this.state === 'half-open') {
44
+ this.state = 'closed';
45
+ this.samples = [];
46
+ }
47
+ }
48
+ onFailure() {
49
+ if (this.state === 'half-open') {
50
+ this.trip();
51
+ }
52
+ }
53
+ p99() {
54
+ if (this.samples.length === 0)
55
+ return 0;
56
+ const sorted = [...this.samples].sort((a, b) => a - b);
57
+ const idx = Math.floor(sorted.length * 0.99);
58
+ return sorted[Math.min(idx, sorted.length - 1)];
59
+ }
60
+ evaluate() {
61
+ if (this.state !== 'closed')
62
+ return;
63
+ if (this.samples.length >= 10 && this.p99() > CIRCUIT_TRIP_MS) {
64
+ this.trip();
65
+ }
66
+ }
67
+ trip() {
68
+ this.state = 'open';
69
+ this.trippedAt = Date.now();
70
+ }
71
+ }
72
+ // ─── Singleton manager ────────────────────────────────────────────────────────
73
+ class CircuitManager {
74
+ circuits = new Map();
75
+ inFlight = 0;
76
+ getCircuit(tool) {
77
+ if (!this.circuits.has(tool))
78
+ this.circuits.set(tool, new ToolCircuit());
79
+ return this.circuits.get(tool);
80
+ }
81
+ /** Check backpressure BEFORE enqueuing. Returns retryAfterMs or 0 if OK. */
82
+ checkCapacity() {
83
+ return this.inFlight >= QUEUE_CAPACITY ? 5_000 : 0;
84
+ }
85
+ /** True if circuit is open (tool force-gated to async). */
86
+ isForceAsync(tool) {
87
+ return this.getCircuit(tool).isOpen();
88
+ }
89
+ /** Record a completed call. Pass durationMs for p99 tracking. */
90
+ recordComplete(tool, durationMs, success) {
91
+ const c = this.getCircuit(tool);
92
+ c.record(durationMs);
93
+ if (success)
94
+ c.onSuccess();
95
+ else
96
+ c.onFailure();
97
+ this.inFlight = Math.max(0, this.inFlight - 1);
98
+ if (c.state === 'open') {
99
+ nexusEventBus.emit('nexus.circuit-open', { tool, p99Ms: c.p99() });
100
+ }
101
+ }
102
+ /** Call when work starts (increments in-flight). */
103
+ recordStart(tool) {
104
+ this.inFlight++;
105
+ if (this.inFlight >= QUEUE_CAPACITY) {
106
+ nexusEventBus.emit('nexus.circuit-tripped', { tool, reason: 'backpressure', queueDepth: this.inFlight });
107
+ }
108
+ }
109
+ getStats(tool) {
110
+ const c = this.getCircuit(tool);
111
+ return {
112
+ tool,
113
+ state: c.state,
114
+ p99Ms: c.p99(),
115
+ sampleCount: c.samples?.length ?? 0,
116
+ trippedAt: c.state !== 'closed' ? c.trippedAt : undefined,
117
+ };
118
+ }
119
+ getAllStats() {
120
+ return Array.from(this.circuits.keys()).map(t => this.getStats(t));
121
+ }
122
+ getInFlight() {
123
+ return this.inFlight;
124
+ }
125
+ }
126
+ let _manager = null;
127
+ export function getCircuitManager() {
128
+ if (!_manager)
129
+ _manager = new CircuitManager();
130
+ return _manager;
131
+ }
132
+ export function checkBackpressure() {
133
+ const retryAfterMs = getCircuitManager().checkCapacity();
134
+ return retryAfterMs > 0 ? { ok: false, retryAfterMs } : { ok: true };
135
+ }
@@ -1050,6 +1050,108 @@ export function buildMcpToolDefinitions() {
1050
1050
  },
1051
1051
  ...synapseToolDefinitions,
1052
1052
  ...architectsToolDefinitions,
1053
+ // ── Workforce (unified worker+job layer) ──────────────────────────
1054
+ {
1055
+ name: 'nexus_work_enqueue',
1056
+ description: 'Enqueue a new job into the workforce queue. Jobs with dependsOn start in backlog; others start ready.',
1057
+ inputSchema: {
1058
+ type: 'object',
1059
+ properties: {
1060
+ title: { type: 'string' },
1061
+ tier: { type: 'string', enum: ['strategy', 'tactical'] },
1062
+ priority: { type: 'number' },
1063
+ parentId: { type: 'string' },
1064
+ dependsOn: { type: 'array', items: { type: 'string' } },
1065
+ budgetCapUsd: { type: 'number' },
1066
+ payload: { type: 'object' },
1067
+ client: { type: 'string' },
1068
+ },
1069
+ required: ['title'],
1070
+ },
1071
+ },
1072
+ {
1073
+ name: 'nexus_work_claim',
1074
+ description: 'Atomically claim a specific job (by jobId) or the next highest-priority ready job.',
1075
+ inputSchema: {
1076
+ type: 'object',
1077
+ properties: {
1078
+ workerId: { type: 'string' },
1079
+ jobId: { type: 'string' },
1080
+ leaseMs: { type: 'number' },
1081
+ },
1082
+ required: ['workerId'],
1083
+ },
1084
+ },
1085
+ {
1086
+ name: 'nexus_work_progress',
1087
+ description: 'Advance a claimed job (claimed→wip or wip→review) and record token usage.',
1088
+ inputSchema: {
1089
+ type: 'object',
1090
+ properties: {
1091
+ jobId: { type: 'string' },
1092
+ workerId: { type: 'string' },
1093
+ stage: { type: 'string', enum: ['wip', 'review'] },
1094
+ tokens: { type: 'number' },
1095
+ },
1096
+ required: ['jobId', 'workerId'],
1097
+ },
1098
+ },
1099
+ {
1100
+ name: 'nexus_work_complete',
1101
+ description: 'Mark a job as done, failed, or blocked. Triggers DAG promotion.',
1102
+ inputSchema: {
1103
+ type: 'object',
1104
+ properties: {
1105
+ jobId: { type: 'string' },
1106
+ workerId: { type: 'string' },
1107
+ outcome: { type: 'string', enum: ['done', 'failed', 'blocked'] },
1108
+ },
1109
+ required: ['jobId', 'workerId', 'outcome'],
1110
+ },
1111
+ },
1112
+ {
1113
+ name: 'nexus_work_block',
1114
+ description: 'Mark a job as blocked.',
1115
+ inputSchema: {
1116
+ type: 'object',
1117
+ properties: {
1118
+ jobId: { type: 'string' },
1119
+ workerId: { type: 'string' },
1120
+ },
1121
+ required: ['jobId', 'workerId'],
1122
+ },
1123
+ },
1124
+ {
1125
+ name: 'nexus_work_kanban',
1126
+ description: 'Kanban board showing jobs grouped by status lane.',
1127
+ inputSchema: {
1128
+ type: 'object',
1129
+ properties: { client: { type: 'string' } },
1130
+ },
1131
+ },
1132
+ {
1133
+ name: 'nexus_work_workers',
1134
+ description: 'List registered workers with status, load, and budget.',
1135
+ inputSchema: {
1136
+ type: 'object',
1137
+ properties: {
1138
+ status: { type: 'string', enum: ['idle', 'active', 'stale', 'retired'] },
1139
+ },
1140
+ },
1141
+ },
1142
+ {
1143
+ name: 'nexus_work_hire',
1144
+ description: 'Register a new worker (agent) into the workforce pool.',
1145
+ inputSchema: {
1146
+ type: 'object',
1147
+ properties: {
1148
+ name: { type: 'string' },
1149
+ role: { type: 'string' },
1150
+ client: { type: 'string' },
1151
+ budgetCapUsd: { type: 'number' },
1152
+ },
1153
+ },
1154
+ },
1053
1155
  // ── Embedded capabilities (auto-detected from installed skills) ──
1054
1156
  ...getGstackBridge().getToolDefinitions(),
1055
1157
  ];