@orboto/mcp 0.195.1 → 0.197.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/dist/event-bridge.js +5 -0
- package/dist/event-bridge.test.js +34 -0
- package/dist/http-transport.js +6 -3
- package/dist/manifest-size.test.js +4 -2
- package/dist/orboto-client.js +1 -1
- package/dist/server.js +10 -0
- package/dist/tool-docs.js +1 -1
- package/dist/tools/agent-coordination.js +17 -23
- package/dist/tools/agent-coordination.test.js +40 -6
- package/dist/tools/agent-message-work.js +22 -0
- package/dist/tools/agent-message-work.test.js +33 -0
- package/dist/tools/agent-messages.js +18 -8
- package/dist/tools/agent-presence-schema.js +35 -0
- package/dist/tools/claim.js +22 -19
- package/dist/tools/claim.test.js +7 -5
- package/dist/tools/get-ticket.js +3 -0
- package/dist/tools/git-rotate-token.js +61 -0
- package/dist/tools/git-rotate-token.test.js +87 -0
- package/dist/tools/list-git-app-installations.js +1 -10
- package/dist/tools/list-tickets.js +4 -0
- package/dist/tools/project-readiness-schema.js +16 -0
- package/dist/tools/project-readiness.js +16 -0
- package/dist/tools/project-readiness.test.js +19 -0
- package/dist/tools/raci.js +4 -2
- package/dist/tools/sentry.js +85 -0
- package/dist/tools/sentry.test.js +107 -0
- package/dist/tools/shared.js +9 -0
- package/dist/tools/spec-check.js +15 -0
- package/dist/tools/spec-check.test.js +23 -0
- package/dist/tools/spec-readiness.test.js +85 -0
- package/dist/tools/spec-schemas.js +34 -0
- package/dist/tools/ticket-writes.js +27 -22
- package/dist/tools/ticket-writes.test.js +3 -4
- package/dist/tools/update-project.js +19 -12
- package/dist/tools/update-project.test.js +13 -4
- package/dist/tools/work-next.js +1 -1
- package/dist/tools/work-sessions-next.test.js +23 -0
- package/dist/tools/work-sessions.js +30 -9
- package/dist/tools/work-sessions.test.js +14 -0
- package/package.json +1 -1
package/dist/event-bridge.js
CHANGED
|
@@ -67,6 +67,10 @@ export class EventBridge {
|
|
|
67
67
|
}
|
|
68
68
|
catch { /* ignore */ } });
|
|
69
69
|
}
|
|
70
|
+
/** ORB-2132 - pin the instance token once the transport session id is known; applies to the next (re)connect. */
|
|
71
|
+
setInstanceToken(token) {
|
|
72
|
+
this.opts = { ...this.opts, instanceToken: token };
|
|
73
|
+
}
|
|
70
74
|
/** Open the SSE stream and start forwarding. Resolves immediately;
|
|
71
75
|
* the actual forwarding happens on the returned background loop. */
|
|
72
76
|
start() {
|
|
@@ -114,6 +118,7 @@ export class EventBridge {
|
|
|
114
118
|
headers: {
|
|
115
119
|
Authorization: `Bearer ${bearer}`,
|
|
116
120
|
Accept: 'text/event-stream',
|
|
121
|
+
...(this.opts.instanceToken ? { 'x-orboto-agent-session': this.opts.instanceToken } : {}),
|
|
117
122
|
},
|
|
118
123
|
signal: this.abort.signal,
|
|
119
124
|
});
|
|
@@ -144,3 +144,37 @@ describe('EventBridge.dispatch', () => {
|
|
|
144
144
|
expect(mcp.sendResourceUpdated).toHaveBeenCalledTimes(2);
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
describe('ORB-2132 - the bridge identifies its tool session', () => {
|
|
148
|
+
async function connectOnce(opts) {
|
|
149
|
+
const fetchFn = vi.fn().mockResolvedValue(mockSseResponse([]));
|
|
150
|
+
const bridge = new EventBridge({
|
|
151
|
+
baseUrl: 'https://orboto.example.com',
|
|
152
|
+
apiKey: 'orb_x',
|
|
153
|
+
...(opts.instanceToken ? { instanceToken: opts.instanceToken } : {}),
|
|
154
|
+
mcp: mockMcp(),
|
|
155
|
+
subscriptions: new Set(),
|
|
156
|
+
fetchFn: fetchFn,
|
|
157
|
+
log: () => { },
|
|
158
|
+
});
|
|
159
|
+
if (opts.pin)
|
|
160
|
+
bridge.setInstanceToken(opts.pin);
|
|
161
|
+
bridge.start();
|
|
162
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
163
|
+
bridge.close();
|
|
164
|
+
expect(fetchFn).toHaveBeenCalledTimes(1);
|
|
165
|
+
const [url, init] = fetchFn.mock.calls[0];
|
|
166
|
+
expect(url).toBe('https://orboto.example.com/sse/mcp-events');
|
|
167
|
+
return init.headers;
|
|
168
|
+
}
|
|
169
|
+
it('sends x-orboto-agent-session with the instance token given at construction', async () => {
|
|
170
|
+
expect(await connectOnce({ instanceToken: 'mcp-session-1' })).toMatchObject({
|
|
171
|
+
Authorization: 'Bearer orb_x', 'x-orboto-agent-session': 'mcp-session-1',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
it('sends the token pinned after construction, once the transport session id is known', async () => {
|
|
175
|
+
expect((await connectOnce({ pin: 'mcp-session-2' }))['x-orboto-agent-session']).toBe('mcp-session-2');
|
|
176
|
+
});
|
|
177
|
+
it('omits the header when no instance token is known', async () => {
|
|
178
|
+
expect(await connectOnce({})).not.toHaveProperty('x-orboto-agent-session');
|
|
179
|
+
});
|
|
180
|
+
});
|
package/dist/http-transport.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildOrbotoMcpServer } from './server.js';
|
|
|
11
11
|
import { resolveToolset } from './toolset.js';
|
|
12
12
|
import { OrbotoClient, preflightMcpSession } from './orboto-client.js';
|
|
13
13
|
import { EventBridge } from './event-bridge.js';
|
|
14
|
+
import { mcpInstanceToken } from './tools/shared.js';
|
|
14
15
|
/** Default store backed by the api's `/system/mcp/sessions` endpoints. Each
|
|
15
16
|
* call builds a short-lived OrbotoClient bound to the request's token so the
|
|
16
17
|
* api scopes every read/write to that identity. */
|
|
@@ -167,12 +168,13 @@ function wwwAuthChallenge(req, baseUrl, error, description) {
|
|
|
167
168
|
export function createHttpServer({ baseUrl, sessionStore }) {
|
|
168
169
|
const sessions = new Map();
|
|
169
170
|
const store = sessionStore ?? createApiSessionStore(baseUrl);
|
|
170
|
-
async function buildSessionCore(tokenHolder, userAgentSuffix, toolset) {
|
|
171
|
+
async function buildSessionCore(tokenHolder, userAgentSuffix, toolset, sessionId) {
|
|
171
172
|
const tokenProvider = holderTokenProvider(tokenHolder);
|
|
172
173
|
const sessionClient = new OrbotoClient({ baseUrl, tokenProvider, userAgentSuffix });
|
|
173
174
|
const subscriptions = new Set();
|
|
174
175
|
const mcp = await buildOrbotoMcpServer({ baseUrl, tokenProvider, userAgentSuffix, subscriptions, toolset });
|
|
175
|
-
const bridge = new EventBridge({ baseUrl, tokenProvider, mcp, subscriptions
|
|
176
|
+
const bridge = new EventBridge({ baseUrl, tokenProvider, mcp, subscriptions,
|
|
177
|
+
...(sessionId ? { instanceToken: mcpInstanceToken(undefined, { sessionId }) } : {}) });
|
|
176
178
|
return { sessionClient, subscriptions, mcp, bridge };
|
|
177
179
|
}
|
|
178
180
|
/** ORB-1576 - resolve a bearer's owner identity via the status preflight;
|
|
@@ -188,7 +190,7 @@ export function createHttpServer({ baseUrl, sessionStore }) {
|
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
async function establishForcedSession(tokenHolder, userAgentSuffix, chosenSessionId, userEmail, toolset) {
|
|
191
|
-
const { sessionClient, mcp, bridge } = await buildSessionCore(tokenHolder, userAgentSuffix, toolset);
|
|
193
|
+
const { sessionClient, mcp, bridge } = await buildSessionCore(tokenHolder, userAgentSuffix, toolset, chosenSessionId);
|
|
192
194
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
|
193
195
|
transport.onclose = () => { sessions.delete(chosenSessionId); bridge.close(); };
|
|
194
196
|
await mcp.connect(transport);
|
|
@@ -373,6 +375,7 @@ export function createHttpServer({ baseUrl, sessionStore }) {
|
|
|
373
375
|
sessions.set(sid, {
|
|
374
376
|
transport, mcp, client: sessionClient, bridge, tokenHolder, userEmail: ownerEmail, lastTouchAt: Date.now(),
|
|
375
377
|
});
|
|
378
|
+
bridge.setInstanceToken(mcpInstanceToken(undefined, { sessionId: sid }));
|
|
376
379
|
bridge.start();
|
|
377
380
|
void store
|
|
378
381
|
.register(token, { sessionId: sid, clientInfo, userAgent })
|
|
@@ -24,12 +24,14 @@ afterEach(() => { vi.restoreAllMocks(); });
|
|
|
24
24
|
* edits - a description essay creeping back in is exactly what these
|
|
25
25
|
* must catch, so the headroom is deliberately tighter than the old
|
|
26
26
|
* 20 %. Shrink-only: raising either number is a conscious decision to
|
|
27
|
-
* grow every session's connect cost.
|
|
27
|
+
* grow every session's connect cost. ORB-2132 raised the full ceiling by
|
|
28
|
+
* 2,000 chars because the agent_presence output schema now carries kind,
|
|
29
|
+
* actsAs, projects and connections (measured full = 165,759 chars).
|
|
28
30
|
*
|
|
29
31
|
* @see ORB-1805, ORB-1669, ORB-1910
|
|
30
32
|
*/
|
|
31
33
|
const CURATED_MAX_CHARS = 37_500;
|
|
32
|
-
const FULL_MAX_CHARS =
|
|
34
|
+
const FULL_MAX_CHARS = 167_000;
|
|
33
35
|
/**
|
|
34
36
|
* ORB-1805 - the estimator the ticket measured the failure with
|
|
35
37
|
* (`n_keep: 13533 >= n_ctx: 8192` in LM Studio): characters / 3.6. It is
|
package/dist/orboto-client.js
CHANGED
|
@@ -80,7 +80,7 @@ export class OrbotoClient {
|
|
|
80
80
|
}
|
|
81
81
|
/** GET a JSON endpoint. Throws `OrbotoApiError` on non-2xx. */
|
|
82
82
|
async get(path, options = {}) {
|
|
83
|
-
const res = await this.authedFetch(this.fullUrl(path), { method: 'GET', signal: options.signal });
|
|
83
|
+
const res = await this.authedFetch(this.fullUrl(path), { method: 'GET', signal: options.signal, ...(options.instanceToken ? { headers: { 'x-orboto-agent-session': options.instanceToken } } : {}) });
|
|
84
84
|
return (await res.json());
|
|
85
85
|
}
|
|
86
86
|
/** POST a JSON body. */
|
package/dist/server.js
CHANGED
|
@@ -23,9 +23,11 @@ import { apiCallToolConfig, makeApiCallHandler } from './tools/api-call.js';
|
|
|
23
23
|
import { listAgentInstructionsToolConfig, makeListAgentInstructionsHandler, createAgentInstructionToolConfig, makeCreateAgentInstructionHandler, updateAgentInstructionToolConfig, makeUpdateAgentInstructionHandler, resetAgentInstructionToolConfig, makeResetAgentInstructionHandler, deleteAgentInstructionToolConfig, makeDeleteAgentInstructionHandler, } from './tools/agent-instructions.js';
|
|
24
24
|
import { agentHeartbeatToolConfig, makeAgentHeartbeatHandler, agentPresenceToolConfig, makeAgentPresenceHandler, agentNotifyToolConfig, makeAgentNotifyHandler, } from './tools/agent-coordination.js';
|
|
25
25
|
import { agentBroadcastToolConfig, makeAgentBroadcastHandler, } from './tools/agent-coordination.js';
|
|
26
|
+
import { agentMessageWorkToolConfig, makeAgentMessageWorkHandler } from './tools/agent-message-work.js';
|
|
26
27
|
import { agentMessagesToolConfig, makeAgentMessagesHandler } from './tools/agent-messages.js';
|
|
27
28
|
import { listProjectsToolConfig, makeListProjectsHandler } from './tools/list-projects.js';
|
|
28
29
|
import { getProjectToolConfig, makeGetProjectHandler } from './tools/get-project.js';
|
|
30
|
+
import { projectReadinessToolConfig, makeProjectReadinessHandler } from './tools/project-readiness.js';
|
|
29
31
|
import { getProjectPrimerToolConfig, makeGetProjectPrimerHandler } from './tools/get-project-primer.js';
|
|
30
32
|
import { listTicketsToolConfig, makeListTicketsHandler } from './tools/list-tickets.js';
|
|
31
33
|
import { criticalPathToolConfig, makeCriticalPathHandler } from './tools/critical-path.js';
|
|
@@ -39,6 +41,7 @@ import { searchToolConfig, makeSearchHandler } from './tools/search.js';
|
|
|
39
41
|
import { queryToolConfig, makeQueryHandler } from './tools/query.js';
|
|
40
42
|
import { customerReportToolConfig, makeCustomerReportHandler } from './tools/customer-report.js';
|
|
41
43
|
import { requirementsSpecToolConfig, makeRequirementsSpecHandler } from './tools/requirements-spec.js';
|
|
44
|
+
import { specCheckToolConfig, makeSpecCheckHandler } from './tools/spec-check.js';
|
|
42
45
|
import { wikiIngestUrlToolConfig, makeWikiIngestUrlHandler, wikiAskToolConfig, makeWikiAskHandler, wikiLintToolConfig, makeWikiLintHandler, wikiPlanUpdateToolConfig, makeWikiPlanUpdateHandler, wikiApplyPlanToolConfig, makeWikiApplyPlanHandler, wikiRecordToolConfig, makeWikiRecordHandler, wikiAppendSectionToolConfig, makeWikiAppendSectionHandler, wikiFlagStaleToolConfig, makeWikiFlagStaleHandler, wikiSaveAnswerToolConfig, makeWikiSaveAnswerHandler, } from './tools/wiki.js';
|
|
43
46
|
import { personalFactListToolConfig, makePersonalFactListHandler, personalFactAddToolConfig, makePersonalFactAddHandler, personalFactUpdateToolConfig, makePersonalFactUpdateHandler, personalFactDeleteToolConfig, makePersonalFactDeleteHandler, } from './tools/personal-facts.js';
|
|
44
47
|
import { listDocSpacesToolConfig, makeListDocSpacesHandler, getDocToolConfig, makeGetDocHandler, createDocSpaceToolConfig, makeCreateDocSpaceHandler, updateDocSpaceToolConfig, makeUpdateDocSpaceHandler, deleteDocSpaceToolConfig, makeDeleteDocSpaceHandler, listDocsInSpaceToolConfig, makeListDocsInSpaceHandler, createDocToolConfig, makeCreateDocHandler, updateDocToolConfig, makeUpdateDocHandler, deleteDocToolConfig, makeDeleteDocHandler, moveDocToolConfig, makeMoveDocHandler, duplicateDocSpaceToolConfig, makeDuplicateDocSpaceHandler, resolveDocSmartLinksToolConfig, makeResolveDocSmartLinksHandler, } from './tools/docs.js';
|
|
@@ -53,6 +56,8 @@ import { listCrossProjectLinksToolConfig, makeListCrossProjectLinksHandler, addC
|
|
|
53
56
|
import { getTimerToolConfig, makeGetTimerHandler } from './tools/get-timer.js';
|
|
54
57
|
import { getChecklistsToolConfig, makeGetChecklistsHandler } from './tools/get-checklists.js';
|
|
55
58
|
import { listGitAppInstallationsToolConfig, makeListGitAppInstallationsHandler } from './tools/list-git-app-installations.js';
|
|
59
|
+
import { gitRotateTokenToolConfig, makeGitRotateTokenHandler } from './tools/git-rotate-token.js';
|
|
60
|
+
import { sentryToolConfig, makeSentryHandler } from './tools/sentry.js';
|
|
56
61
|
import { createTicketToolConfig, makeCreateTicketHandler, updateTicketToolConfig, makeUpdateTicketHandler, moveTicketToolConfig, makeMoveTicketHandler, closeTicketToolConfig, makeCloseTicketHandler, deleteTicketToolConfig, makeDeleteTicketHandler, commentToolConfig, makeCommentHandler, updateCommentToolConfig, makeUpdateCommentHandler, deleteCommentToolConfig, makeDeleteCommentHandler, assignToolConfig, makeAssignHandler, unassignToolConfig, makeUnassignHandler, labelTicketToolConfig, makeLabelTicketHandler, unlabelTicketToolConfig, makeUnlabelTicketHandler, setMilestoneToolConfig, makeSetMilestoneHandler, addTicketDependencyToolConfig, makeAddTicketDependencyHandler, removeTicketDependencyToolConfig, makeRemoveTicketDependencyHandler, listTicketDependenciesToolConfig, makeListTicketDependenciesHandler, } from './tools/ticket-writes.js';
|
|
57
62
|
import { timerStartToolConfig, makeTimerStartHandler, timerStopToolConfig, makeTimerStopHandler, logTimeToolConfig, makeLogTimeHandler, listTimeEntriesToolConfig, makeListTimeEntriesHandler, editTimeEntryToolConfig, makeEditTimeEntryHandler, deleteTimeEntryToolConfig, makeDeleteTimeEntryHandler, } from './tools/time-writes.js';
|
|
58
63
|
import { listTicketSchedulesToolConfig, makeListTicketSchedulesHandler, scheduleTicketSessionToolConfig, makeScheduleTicketSessionHandler, cancelTicketSessionToolConfig, makeCancelTicketSessionHandler, } from './tools/ticket-schedules.js';
|
|
@@ -191,6 +196,7 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
191
196
|
reg('orboto_agent_heartbeat', agentHeartbeatToolConfig, makeAgentHeartbeatHandler(client));
|
|
192
197
|
reg('orboto_agent_presence', agentPresenceToolConfig, makeAgentPresenceHandler(client));
|
|
193
198
|
reg('orboto_agent_notify', agentNotifyToolConfig, makeAgentNotifyHandler(client));
|
|
199
|
+
reg('orboto_message_work', agentMessageWorkToolConfig, makeAgentMessageWorkHandler(client));
|
|
194
200
|
reg('orboto_messages', agentMessagesToolConfig, makeAgentMessagesHandler(client));
|
|
195
201
|
reg('orboto_agent_broadcast', agentBroadcastToolConfig, makeAgentBroadcastHandler(client));
|
|
196
202
|
reg('orboto_list_projects', listProjectsToolConfig, makeListProjectsHandler(client));
|
|
@@ -209,6 +215,7 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
209
215
|
reg('orboto_query', queryToolConfig, makeQueryHandler(client));
|
|
210
216
|
reg('orboto_customer_report', customerReportToolConfig, makeCustomerReportHandler(client));
|
|
211
217
|
reg('orboto_requirements_spec', requirementsSpecToolConfig, makeRequirementsSpecHandler(client));
|
|
218
|
+
reg('orboto_spec_check', specCheckToolConfig, makeSpecCheckHandler(client));
|
|
212
219
|
reg('orboto_list_doc_spaces', listDocSpacesToolConfig, makeListDocSpacesHandler(client));
|
|
213
220
|
reg('orboto_get_doc', getDocToolConfig, makeGetDocHandler(client));
|
|
214
221
|
reg('orboto_search_docs', searchDocsToolConfig, makeSearchDocsHandler(client));
|
|
@@ -261,6 +268,8 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
261
268
|
reg('orboto_personal_fact_delete', personalFactDeleteToolConfig, makePersonalFactDeleteHandler(client));
|
|
262
269
|
reg('orboto_get_timer', getTimerToolConfig, makeGetTimerHandler(client));
|
|
263
270
|
reg('orboto_list_git_app_installations', listGitAppInstallationsToolConfig, makeListGitAppInstallationsHandler(client));
|
|
271
|
+
reg('orboto_git_rotate_token', gitRotateTokenToolConfig, makeGitRotateTokenHandler(client));
|
|
272
|
+
reg('orboto_sentry', sentryToolConfig, makeSentryHandler(client));
|
|
264
273
|
reg('orboto_create_ticket', createTicketToolConfig, makeCreateTicketHandler(client));
|
|
265
274
|
reg('orboto_update_ticket', updateTicketToolConfig, makeUpdateTicketHandler(client));
|
|
266
275
|
reg('orboto_move_ticket', moveTicketToolConfig, makeMoveTicketHandler(client));
|
|
@@ -336,6 +345,7 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
336
345
|
reg('orboto_attach_to_ticket', attachToTicketToolConfig, makeAttachToTicketHandler(client));
|
|
337
346
|
reg('orboto_set_parent', setParentToolConfig, makeSetParentHandler(client));
|
|
338
347
|
reg('orboto_update_project', updateProjectToolConfig, makeUpdateProjectHandler(client));
|
|
348
|
+
reg('orboto_project_readiness', projectReadinessToolConfig, makeProjectReadinessHandler(client));
|
|
339
349
|
reg('orboto_create_project', createProjectToolConfig, makeCreateProjectHandler(client));
|
|
340
350
|
reg('orboto_archive_project', archiveProjectToolConfig, makeArchiveProjectHandler(client));
|
|
341
351
|
reg('orboto_check_similar', checkSimilarToolConfig, makeCheckSimilarHandler(client));
|
package/dist/tool-docs.js
CHANGED
|
@@ -13,7 +13,7 @@ export const SUMMARY_MAX_CHARS = 220;
|
|
|
13
13
|
*/
|
|
14
14
|
const SUMMARY_OVERRIDES = {
|
|
15
15
|
orboto_create_full_backup: 'Start a full-workspace backup job (all projects, users, config, attachments); returns the job id to poll via orboto_list_backups.',
|
|
16
|
-
orboto_get_ticket: 'Fetch one ticket by key
|
|
16
|
+
orboto_get_ticket: 'Fetch one ticket by key or UUID with description, status, assignees, checklists and comments.',
|
|
17
17
|
orboto_requirements_spec: 'Generate a structured requirements spec for a project or milestone from its tickets (scope, actors, functional + non-functional requirements).',
|
|
18
18
|
orboto_update_doc_space: 'Update a doc space\'s name, description, icon, project binding or access mode (open/restricted) by space id.',
|
|
19
19
|
orboto_search_docs: 'Full-text search over doc/wiki pages (query, optional space or project filter); returns matching pages with snippets.',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @see ORB-704, ORB-706
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { AgentInventoryEntrySchema, AgentInventoryKindSchema } from './agent-presence-schema.js';
|
|
7
8
|
import { mcpInstanceToken } from './shared.js';
|
|
8
9
|
export const agentHeartbeatToolConfig = {
|
|
9
10
|
title: 'Agent heartbeat (Multi-Agent Coordination)',
|
|
@@ -37,35 +38,28 @@ export function makeAgentHeartbeatHandler(client) {
|
|
|
37
38
|
}
|
|
38
39
|
export const agentPresenceToolConfig = {
|
|
39
40
|
title: 'Workspace agent presence',
|
|
40
|
-
description: 'Returns
|
|
41
|
-
inputSchema: z.object({
|
|
41
|
+
description: 'Returns all active orboto agent instances, including lane workers/reviewers, inbox and MCP connections and registered runners. Session freshness is 90 seconds; lanes retain their 300-second heartbeat grace. Unexpired work leases keep their owning instance visible. Ticket metadata and claims retain ordinary ticket visibility. Rows include `kind` (agent or human, classified by the credential: agent API key, OAuth grant, lane, external token or bot account; `isBot` is its alias), `actsAs` (the human account an AI client acts under), `projects` (memberships plus lane projects), `connections` (type + label per credential or attachment, `live_events` for the MCP event bridge), owner, lane and stable sessionId; separate instances of one account remain separate. Callers with admin:system:read see every agent; other users see only their own sessions (useful for "is my dispatcher daemon alive?" checks). Each row exposes `userId`, `userEmail`, the agent runtime (`clientInfo.name`), declared `capabilities`, current `status`, and the ticket the agent is working on if any. Use this to plan multi-agent work - e.g. before dispatching a sub-task, look up which other agents are active and what they\'re working on so you don\'t step on a parallel run. Filters `projectKey` (rows whose `projects` contain the key) and `kind` (agent | human) narrow the list client-side, the same way `orboto agents --project KEY --bots|--humans` does.',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
projectKey: z.string().min(1).max(64).optional().describe('Keep only rows whose projects contain this project key (case-insensitive).'),
|
|
44
|
+
kind: AgentInventoryKindSchema.optional().describe('Keep only rows classified as agent or human by their credential.'),
|
|
45
|
+
}).shape,
|
|
42
46
|
outputSchema: z.object({
|
|
43
|
-
sessions: z.array(
|
|
44
|
-
userId: z.string().uuid(),
|
|
45
|
-
userEmail: z.string(),
|
|
46
|
-
userFullName: z.string().nullable(),
|
|
47
|
-
sessionId: z.string().uuid(),
|
|
48
|
-
status: z.string(),
|
|
49
|
-
workingOnTicket: z.object({
|
|
50
|
-
id: z.string().uuid(),
|
|
51
|
-
key: z.string().nullable(),
|
|
52
|
-
title: z.string(),
|
|
53
|
-
projectKey: z.string().nullable(),
|
|
54
|
-
}).nullable(),
|
|
55
|
-
capabilities: z.array(z.string()),
|
|
56
|
-
clientInfo: z.record(z.string(), z.string()),
|
|
57
|
-
lastSeenAt: z.string(),
|
|
58
|
-
createdAt: z.string(),
|
|
59
|
-
})),
|
|
47
|
+
sessions: z.array(AgentInventoryEntrySchema),
|
|
60
48
|
}).shape,
|
|
61
49
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
62
50
|
};
|
|
51
|
+
/** Client-side view filter (ORB-2135); the inventory route stays unfiltered and the chat mirror applies the same rule. */
|
|
52
|
+
export function filterPresenceRows(rows, args) {
|
|
53
|
+
const key = args.projectKey?.trim().toUpperCase();
|
|
54
|
+
return rows.filter((row) => (!args.kind || row.kind === args.kind) && (!key || row.projects.some((p) => p.key.toUpperCase() === key)));
|
|
55
|
+
}
|
|
63
56
|
export function makeAgentPresenceHandler(client) {
|
|
64
|
-
return async () => {
|
|
65
|
-
const
|
|
57
|
+
return async (args = {}) => {
|
|
58
|
+
const all = await client.get('/v1/agent/inventory');
|
|
59
|
+
const sessions = filterPresenceRows(all, args);
|
|
66
60
|
const lines = [];
|
|
67
61
|
if (sessions.length === 0) {
|
|
68
|
-
lines.push('No active agent sessions in the workspace.');
|
|
62
|
+
lines.push(all.length === 0 ? 'No active agent sessions in the workspace.' : `No active agent sessions match the filter (${all.length} active in the workspace).`);
|
|
69
63
|
}
|
|
70
64
|
else {
|
|
71
65
|
lines.push(`${sessions.length} active session(s):`);
|
|
@@ -75,7 +69,7 @@ export function makeAgentPresenceHandler(client) {
|
|
|
75
69
|
const work = s.workingOnTicket
|
|
76
70
|
? ` · working on [${s.workingOnTicket.projectKey ?? '?'}] ${s.workingOnTicket.key ?? s.workingOnTicket.id} (${s.workingOnTicket.title})`
|
|
77
71
|
: '';
|
|
78
|
-
lines.push(`- ${name} (${runtime}) - ${s.status}${work}`);
|
|
72
|
+
lines.push(`- ${name} <${s.userEmail}> (${runtime}, instance ${s.sessionId}) - ${s.status}${work}; owner: ${s.owner?.name ?? s.owner?.email ?? (s.actsAs ? `acts as ${s.actsAs.name ?? s.actsAs.email}` : s.kind === 'agent' ? 'unassigned' : 'self')}${s.lane ? `; lane: ${s.lane.name}` : ''}${s.projects.length ? `; projects: ${s.projects.map((p) => p.key).join(', ')}` : ''}${s.connections.length ? `; connections: ${s.connections.map((c) => c.type).join(', ')}` : ''}`);
|
|
79
73
|
}
|
|
80
74
|
}
|
|
81
75
|
return {
|
|
@@ -43,16 +43,16 @@ describe('orboto_agent_heartbeat', () => {
|
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
describe('orboto_agent_presence', () => {
|
|
46
|
-
it('GETs /v1/agent/
|
|
46
|
+
it('GETs /v1/agent/inventory and renders one line per session', async () => {
|
|
47
47
|
stub([{
|
|
48
48
|
json: [
|
|
49
49
|
{
|
|
50
50
|
userId: '00000000-0000-0000-0000-000000000001',
|
|
51
51
|
userEmail: 'alice@x.test',
|
|
52
|
-
userFullName: 'Alice',
|
|
52
|
+
userFullName: 'Alice', kind: 'human', isBot: false, actsAs: null, owner: null, autonomyPaused: false, lane: null, projects: [], connections: [], workSessions: [],
|
|
53
53
|
sessionId: '00000000-0000-0000-0000-000000000010',
|
|
54
54
|
status: 'working',
|
|
55
|
-
workingOnTicket: { id: '
|
|
55
|
+
workingOnTicket: { id: '00000000-0000-0000-0000-000000000020', key: 'ORB-42', title: 'Test', projectKey: 'ORB' },
|
|
56
56
|
capabilities: ['writes-tickets'],
|
|
57
57
|
clientInfo: { name: 'claude-code' },
|
|
58
58
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -61,7 +61,8 @@ describe('orboto_agent_presence', () => {
|
|
|
61
61
|
{
|
|
62
62
|
userId: '00000000-0000-0000-0000-000000000002',
|
|
63
63
|
userEmail: 'bot@x.test',
|
|
64
|
-
userFullName: null,
|
|
64
|
+
userFullName: null, kind: 'agent', isBot: true, actsAs: null, owner: { id: '00000000-0000-0000-0000-000000000001', name: 'Alice', email: 'alice@x.test' },
|
|
65
|
+
autonomyPaused: false, lane: null, projects: [{ id: '00000000-0000-0000-0000-000000000030', key: 'ORB', name: 'orboto' }], connections: [{ type: 'api_key', label: 'dispatcher' }], workSessions: [],
|
|
65
66
|
sessionId: '00000000-0000-0000-0000-000000000011',
|
|
66
67
|
status: 'idle',
|
|
67
68
|
workingOnTicket: null,
|
|
@@ -76,8 +77,9 @@ describe('orboto_agent_presence', () => {
|
|
|
76
77
|
const result = await handler();
|
|
77
78
|
const text = result.content[0].text;
|
|
78
79
|
expect(text).toContain('2 active session(s)');
|
|
79
|
-
expect(text).toContain('Alice (claude-code) - working · working on [ORB] ORB-42');
|
|
80
|
-
expect(text).toContain('bot@x.test (dispatcher-daemon) - idle');
|
|
80
|
+
expect(text).toContain('Alice <alice@x.test> (claude-code, instance 00000000-0000-0000-0000-000000000010) - working · working on [ORB] ORB-42');
|
|
81
|
+
expect(text).toContain('bot@x.test <bot@x.test> (dispatcher-daemon, instance 00000000-0000-0000-0000-000000000011) - idle; owner: Alice');
|
|
82
|
+
expect(vi.mocked(fetch).mock.calls[0][0]).toBe('https://orboto.example.com/v1/agent/inventory');
|
|
81
83
|
expect(result.structuredContent.sessions).toHaveLength(2);
|
|
82
84
|
});
|
|
83
85
|
it('handles an empty workspace gracefully', async () => {
|
|
@@ -86,6 +88,38 @@ describe('orboto_agent_presence', () => {
|
|
|
86
88
|
const result = await handler();
|
|
87
89
|
expect(result.content[0].text).toBe('No active agent sessions in the workspace.');
|
|
88
90
|
});
|
|
91
|
+
const presenceRows = () => [
|
|
92
|
+
{ userId: '00000000-0000-0000-0000-000000000001', userEmail: 'alice@x.test', userFullName: 'Alice', kind: 'human', isBot: false, actsAs: null, owner: null, autonomyPaused: false, lane: null, projects: [{ id: '00000000-0000-0000-0000-000000000030', key: 'ORB', name: 'orboto' }], connections: [{ type: 'api_key', label: 'laptop' }], workSessions: [], sessionId: '00000000-0000-0000-0000-000000000010', status: 'working', workingOnTicket: null, capabilities: [], clientInfo: { name: 'claude-code' }, lastSeenAt: new Date().toISOString(), createdAt: new Date().toISOString() },
|
|
93
|
+
{ userId: '00000000-0000-0000-0000-000000000002', userEmail: 'worker@x.test', userFullName: null, kind: 'agent', isBot: true, actsAs: null, owner: { id: '00000000-0000-0000-0000-000000000001', name: 'Alice', email: 'alice@x.test' }, autonomyPaused: false, lane: { id: '00000000-0000-0000-0000-000000000040', name: 'worker-1', role: 'implementation', paused: false }, projects: [{ id: '00000000-0000-0000-0000-000000000031', key: 'ACME', name: 'Acme' }, { id: '00000000-0000-0000-0000-000000000030', key: 'ORB', name: 'orboto' }], connections: [{ type: 'lane', label: 'worker-1' }, { type: 'live_events', label: 'MCP live events' }], workSessions: [], sessionId: '00000000-0000-0000-0000-000000000011', status: 'idle', workingOnTicket: null, capabilities: [], clientInfo: { name: 'pi' }, lastSeenAt: new Date().toISOString(), createdAt: new Date().toISOString() },
|
|
94
|
+
{ userId: '00000000-0000-0000-0000-000000000001', userEmail: 'alice@x.test', userFullName: 'Alice', kind: 'agent', isBot: true, actsAs: { id: '00000000-0000-0000-0000-000000000001', name: 'Alice', email: 'alice@x.test' }, owner: null, autonomyPaused: false, lane: null, projects: [], connections: [{ type: 'oauth', label: 'claude.ai' }], workSessions: [], sessionId: '00000000-0000-0000-0000-000000000012', status: 'idle', workingOnTicket: null, capabilities: [], clientInfo: {}, lastSeenAt: new Date().toISOString(), createdAt: new Date().toISOString() },
|
|
95
|
+
];
|
|
96
|
+
it('filters by projectKey (case-insensitive, projects only) and kind on the client side (ORB-2135)', async () => {
|
|
97
|
+
const handler = makeAgentPresenceHandler(client);
|
|
98
|
+
const ids = async (args) => {
|
|
99
|
+
stub([{ json: presenceRows() }]);
|
|
100
|
+
const result = await handler(args);
|
|
101
|
+
expect(vi.mocked(fetch).mock.calls.at(-1)?.[0]).toBe('https://orboto.example.com/v1/agent/inventory');
|
|
102
|
+
return result.structuredContent.sessions.map((s) => s.sessionId.slice(-2));
|
|
103
|
+
};
|
|
104
|
+
expect(await ids({})).toEqual(['10', '11', '12']);
|
|
105
|
+
expect(await ids({ kind: 'agent' })).toEqual(['11', '12']);
|
|
106
|
+
expect(await ids({ kind: 'human' })).toEqual(['10']);
|
|
107
|
+
expect(await ids({ projectKey: 'acme' })).toEqual(['11']);
|
|
108
|
+
expect(await ids({ projectKey: 'ORB', kind: 'human' })).toEqual(['10']);
|
|
109
|
+
expect(await ids({ projectKey: 'NONE' })).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
it('says when a filter matched nothing but the workspace is not empty', async () => {
|
|
112
|
+
stub([{ json: presenceRows() }]);
|
|
113
|
+
const result = await makeAgentPresenceHandler(client)({ projectKey: 'NONE' });
|
|
114
|
+
expect(result.content[0].text).toBe('No active agent sessions match the filter (3 active in the workspace).');
|
|
115
|
+
});
|
|
116
|
+
it('renders acts-as, lane, projects and connection types on each line', async () => {
|
|
117
|
+
stub([{ json: presenceRows() }]);
|
|
118
|
+
const text = (await makeAgentPresenceHandler(client)({ kind: 'agent' })).content[0].text;
|
|
119
|
+
expect(text).toContain('2 active session(s)');
|
|
120
|
+
expect(text).toContain('owner: Alice; lane: worker-1; projects: ACME, ORB; connections: lane, live_events');
|
|
121
|
+
expect(text).toContain('owner: acts as Alice; connections: oauth');
|
|
122
|
+
});
|
|
89
123
|
});
|
|
90
124
|
describe('orboto_agent_notify', () => {
|
|
91
125
|
it('POSTs to /v1/agent/notify with the body the user supplied', async () => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { mcpInstanceToken } from './shared.js';
|
|
3
|
+
export const agentMessageWorkToolConfig = {
|
|
4
|
+
title: 'Inspect or explicitly handle message work',
|
|
5
|
+
description: 'List, inspect or mutate durable message work (mutation schema: orboto_api_search). ACK is a separate read receipt and finishes nothing.',
|
|
6
|
+
inputSchema: z.object({ messageId: z.string().uuid().optional(), mutation: z.record(z.unknown()).optional(),
|
|
7
|
+
cursor: z.string().optional(), limit: z.number().int().min(1).max(100).default(25), openOnly: z.boolean().default(true) }).shape,
|
|
8
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
9
|
+
};
|
|
10
|
+
export function makeAgentMessageWorkHandler(client) {
|
|
11
|
+
return async (args, extra) => {
|
|
12
|
+
if (args.mutation && !args.messageId)
|
|
13
|
+
throw new Error('messageId is required for a mutation');
|
|
14
|
+
const q = new URLSearchParams({ limit: String(args.limit ?? 25), openOnly: String(args.openOnly ?? true) });
|
|
15
|
+
if (args.cursor)
|
|
16
|
+
q.set('cursor', args.cursor);
|
|
17
|
+
const result = args.mutation
|
|
18
|
+
? await client.post(`/v1/agent/messages/${args.messageId}/work`, { ...args.mutation, instanceToken: mcpInstanceToken(undefined, extra) })
|
|
19
|
+
: await client.get(`${args.messageId ? `/v1/agent/messages/${args.messageId}` : '/v1/agent/message-work'}?${q}`, { instanceToken: mcpInstanceToken(undefined, extra) });
|
|
20
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { agentMessageWorkToolConfig, makeAgentMessageWorkHandler } from './agent-message-work.js';
|
|
4
|
+
import { makeAgentMessagesHandler } from './agent-messages.js';
|
|
5
|
+
describe('message work MCP parity', () => {
|
|
6
|
+
it('carries no second copy of the shared mutation schema', () => {
|
|
7
|
+
const shared = readFileSync(new URL('../../../../packages/shared-schema/src/agent-message-work.ts', import.meta.url), 'utf8');
|
|
8
|
+
const local = readFileSync(new URL('./agent-message-work.ts', import.meta.url), 'utf8');
|
|
9
|
+
const stepBlock = shared.slice(shared.indexOf('export const MessageWorkStepSchema'), shared.indexOf('export const MessageWorkMutationSchema'));
|
|
10
|
+
expect(local).not.toContain(stepBlock.replaceAll('export const', 'const'));
|
|
11
|
+
expect(local).not.toContain('MessageWorkStepSchema');
|
|
12
|
+
expect(agentMessageWorkToolConfig.description).toContain('orboto_api_search');
|
|
13
|
+
});
|
|
14
|
+
it('uses each MCP connection identity and reads exact IDs without ACK', async () => {
|
|
15
|
+
const get = vi.fn().mockResolvedValueOnce({ message: { id: 'message' } }).mockResolvedValue({ messages: [] });
|
|
16
|
+
const post = vi.fn().mockResolvedValue({ state: 'in_work' });
|
|
17
|
+
const client = { get, post };
|
|
18
|
+
const handler = makeAgentMessageWorkHandler(client);
|
|
19
|
+
await handler({ messageId: 'message' }, { sessionId: 'a' });
|
|
20
|
+
expect(get).toHaveBeenCalledWith('/v1/agent/messages/message?limit=25&openOnly=true', { instanceToken: 'mcp-a' });
|
|
21
|
+
expect(post).not.toHaveBeenCalled();
|
|
22
|
+
await handler({ messageId: 'message', mutation: { action: 'claim', summary: 'Explicit acceptance', evidence: [], leaseSeconds: 900 } }, { sessionId: 'a' });
|
|
23
|
+
expect(post).toHaveBeenCalledWith('/v1/agent/messages/message/work', expect.objectContaining({ instanceToken: 'mcp-a' }));
|
|
24
|
+
});
|
|
25
|
+
it('ACK is a plain read receipt - no evidence travels with it', async () => {
|
|
26
|
+
const get = vi.fn().mockResolvedValue({ messages: [] });
|
|
27
|
+
const post = vi.fn().mockResolvedValue({ acked: 1 });
|
|
28
|
+
const client = { get, post };
|
|
29
|
+
await makeAgentMessagesHandler(client)({ ackIds: ['message'] }, { sessionId: 'b' });
|
|
30
|
+
expect(post).toHaveBeenCalledWith('/v1/agent/messages/ack', { ids: ['message'], instanceToken: 'mcp-b' });
|
|
31
|
+
expect(JSON.stringify(post.mock.calls)).not.toContain('evidence');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -8,18 +8,22 @@
|
|
|
8
8
|
* api's `x-orboto-agent-mail` response header - no polling anywhere).
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
+
import { makeAgentMessageWorkHandler } from './agent-message-work.js';
|
|
11
12
|
import { mcpInstanceToken } from './shared.js';
|
|
12
13
|
export const agentMessagesToolConfig = {
|
|
13
|
-
title: '
|
|
14
|
-
description: 'Your
|
|
14
|
+
title: 'Agent inbox and message work',
|
|
15
|
+
description: 'Your inbox. messageWork carries durable work ownership - claim before executing. ackIds is a plain read receipt: no claim needed, finishes nothing. Reply via orboto_agent_notify with threadId.',
|
|
15
16
|
inputSchema: z.object({
|
|
16
|
-
|
|
17
|
+
messageWork: z.record(z.unknown()).optional()
|
|
18
|
+
.describe('Work envelope {messageId, mutation, cursor, limit, openOnly}; schema: orboto_api_search.'),
|
|
19
|
+
all: z.boolean().default(false),
|
|
17
20
|
limit: z.number().int().min(1).max(200).default(50),
|
|
18
|
-
project: z.string().min(1).max(64).optional().describe('Project key
|
|
19
|
-
includeOwnSends: z.boolean().default(false)
|
|
20
|
-
ackIds: z.array(z.string().uuid()).max(200).optional().describe('Message ids to mark
|
|
21
|
+
project: z.string().min(1).max(64).optional().describe('Project key/UUID; includes unscoped mail.'),
|
|
22
|
+
includeOwnSends: z.boolean().default(false),
|
|
23
|
+
ackIds: z.array(z.string().uuid()).max(200).optional().describe('Message ids to mark read.'),
|
|
21
24
|
}).shape,
|
|
22
25
|
outputSchema: z.object({
|
|
26
|
+
work: z.record(z.unknown()).optional(),
|
|
23
27
|
messages: z.array(z.object({
|
|
24
28
|
id: z.string(),
|
|
25
29
|
fromUserId: z.string(),
|
|
@@ -33,13 +37,19 @@ export const agentMessagesToolConfig = {
|
|
|
33
37
|
})),
|
|
34
38
|
acked: z.number().int(),
|
|
35
39
|
}).shape,
|
|
36
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
40
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
37
41
|
};
|
|
38
42
|
export function makeAgentMessagesHandler(client) {
|
|
39
43
|
return async (args, extra) => {
|
|
44
|
+
if (args.messageWork) {
|
|
45
|
+
if (args.ackIds?.length)
|
|
46
|
+
throw new Error('Use a separate explicit ACK call.');
|
|
47
|
+
const result = await makeAgentMessageWorkHandler(client)(args.messageWork, extra);
|
|
48
|
+
return { content: result.content, structuredContent: { messages: [], acked: 0, work: result.structuredContent } };
|
|
49
|
+
}
|
|
40
50
|
let acked = 0;
|
|
41
51
|
if (args.ackIds && args.ackIds.length > 0) {
|
|
42
|
-
const res = await client.post('/v1/agent/messages/ack', { ids: args.ackIds });
|
|
52
|
+
const res = await client.post('/v1/agent/messages/ack', { ids: args.ackIds, instanceToken: mcpInstanceToken(undefined, extra) });
|
|
43
53
|
acked = res.acked;
|
|
44
54
|
}
|
|
45
55
|
const q = new URLSearchParams();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const WorkSessionResourceClaimSchema = z.object({
|
|
3
|
+
kind: z.enum(['path', 'named']), value: z.string().min(1).max(500), mode: z.enum(['read', 'write']),
|
|
4
|
+
state: z.enum(['granted', 'waiting']).optional(), requestedAt: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
export const AgentInventoryKindSchema = z.enum(['agent', 'human']);
|
|
7
|
+
export const AgentConnectionTypeSchema = z.enum(['api_key', 'oauth', 'lane', 'external_token', 'live_events', 'web']);
|
|
8
|
+
export const AgentInventoryConnectionSchema = z.object({ type: AgentConnectionTypeSchema, label: z.string() });
|
|
9
|
+
export const AgentInventoryProjectSchema = z.object({ id: z.string().uuid(), key: z.string(), name: z.string() });
|
|
10
|
+
export const AgentInventoryEntrySchema = z.object({
|
|
11
|
+
userId: z.string().uuid(),
|
|
12
|
+
userEmail: z.string(),
|
|
13
|
+
userFullName: z.string().nullable(),
|
|
14
|
+
kind: AgentInventoryKindSchema,
|
|
15
|
+
/** Alias of `kind === 'agent'`. */
|
|
16
|
+
isBot: z.boolean(),
|
|
17
|
+
actsAs: z.object({ id: z.string().uuid(), name: z.string().nullable(), email: z.string() }).nullable(),
|
|
18
|
+
owner: z.object({ id: z.string().uuid(), name: z.string().nullable(), email: z.string() }).nullable(),
|
|
19
|
+
autonomyPaused: z.boolean(),
|
|
20
|
+
sessionId: z.string().uuid(),
|
|
21
|
+
status: z.string(),
|
|
22
|
+
lane: z.object({ id: z.string().uuid(), name: z.string(), role: z.string(), paused: z.boolean() }).nullable(),
|
|
23
|
+
projects: z.array(AgentInventoryProjectSchema),
|
|
24
|
+
connections: z.array(AgentInventoryConnectionSchema),
|
|
25
|
+
workingOnTicket: z.object({ id: z.string().uuid(), key: z.string().nullable(), title: z.string(), projectKey: z.string().nullable() }).nullable(),
|
|
26
|
+
workSessions: z.array(z.object({
|
|
27
|
+
ticketId: z.string().uuid(), ticketKey: z.string().nullable(), role: z.string(), leaseUntil: z.string(),
|
|
28
|
+
resourceClaims: z.array(WorkSessionResourceClaimSchema),
|
|
29
|
+
})),
|
|
30
|
+
capabilities: z.array(z.string()),
|
|
31
|
+
clientInfo: z.record(z.string(), z.string()),
|
|
32
|
+
lastSeenAt: z.string(),
|
|
33
|
+
createdAt: z.string(),
|
|
34
|
+
});
|
|
35
|
+
export const AgentInventorySchema = z.array(AgentInventoryEntrySchema);
|
package/dist/tools/claim.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @see ORB-179, ORB-181
|
|
5
5
|
*/
|
|
6
|
+
import { specGateFailure, isAlreadyAssigned } from './spec-schemas.js';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { OrbotoApiError } from '../orboto-client.js';
|
|
8
9
|
import { mcpInstanceToken, resolveTicketByKey } from './shared.js';
|
|
@@ -15,33 +16,44 @@ const CATEGORY_TO_LEGACY = {
|
|
|
15
16
|
};
|
|
16
17
|
export const claimToolConfig = {
|
|
17
18
|
title: 'Claim a ticket (assign self + in_progress + timer)',
|
|
18
|
-
description: '
|
|
19
|
-
+ '`agentSessionToken` is a stable per-agent-instance token: on a bot/service account it scopes the timer to YOUR instance - concurrent per-instance timers and NO auto-stop, so you own both start and stop. Omit it on human accounts, which keep the single-timer behaviour.',
|
|
19
|
+
description: 'Assign yourself, move to in_progress and start a timer. Repeated claims recheck the specification gate. Worker bots need ready, or override=true with a reason; enabled epics are blocked. sole removes other assignees after the gate passes. force permits reopening done tickets. noTimer skips time tracking. On human accounts a different active timer is stopped first and its elapsed time booked to that ticket. Bot timers are per instance and never auto-stop each other: agentSessionToken defaults to this MCP connection; own both start and stop.',
|
|
20
20
|
inputSchema: z.object({
|
|
21
21
|
ticketKey: z.string().min(3),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
override: z.boolean().optional().describe("Spec override; reason required."),
|
|
23
|
+
reason: z.string().trim().min(1).max(2000).optional(),
|
|
24
|
+
sole: z.boolean().optional().describe('Remove other assignees after gate.'),
|
|
25
|
+
force: z.boolean().optional().describe('Reopen done.'),
|
|
26
|
+
noTimer: z.boolean().optional().describe('No timer.'),
|
|
27
|
+
agentSessionToken: z.string().optional().describe('Instance token for bot timers.'),
|
|
26
28
|
}).shape,
|
|
27
29
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
28
30
|
};
|
|
29
31
|
export function makeClaimHandler(client) {
|
|
30
|
-
return async ({ ticketKey, sole, force, noTimer, agentSessionToken }, extra) => {
|
|
32
|
+
return async ({ ticketKey, sole, force, override, reason, noTimer, agentSessionToken }, extra) => {
|
|
31
33
|
const me = await client.get('/users/me');
|
|
32
34
|
const current = await resolveTicketByKey(client, ticketKey);
|
|
33
35
|
const currentAssignees = current.assignees ?? [];
|
|
34
|
-
const alreadyAssigned = currentAssignees.some((a) => a.id === me.id);
|
|
36
|
+
const alreadyAssigned = currentAssignees.some((a) => (a.userId ?? a.id) === me.id);
|
|
35
37
|
const currentCategory = current.statusCategory;
|
|
36
38
|
if (currentCategory === 'done' && !force) {
|
|
37
39
|
throw new Error(`Refusing to claim [${current.ticketKey}]: status is "done". Pass force=true to reopen + claim.`);
|
|
38
40
|
}
|
|
41
|
+
try {
|
|
42
|
+
await client.post(`/projects/${current.projectId}/tickets/${current.id}/assignees/${me.id}`, { override, reason });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const gate = error instanceof OrbotoApiError && error.status === 409 ? specGateFailure(error.body) : undefined;
|
|
46
|
+
if (gate)
|
|
47
|
+
return gate;
|
|
48
|
+
if (!(error instanceof OrbotoApiError) || error.status !== 409 || !isAlreadyAssigned(error.body))
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
39
51
|
if (sole) {
|
|
40
52
|
for (const a of currentAssignees) {
|
|
41
|
-
if (a.id === me.id)
|
|
53
|
+
if ((a.userId ?? a.id) === me.id)
|
|
42
54
|
continue;
|
|
43
55
|
try {
|
|
44
|
-
await client.delete(`/projects/${current.projectId}/tickets/${current.id}/assignees/${a.id}`);
|
|
56
|
+
await client.delete(`/projects/${current.projectId}/tickets/${current.id}/assignees/${a.userId ?? a.id}`);
|
|
45
57
|
}
|
|
46
58
|
catch (err) {
|
|
47
59
|
if (!(err instanceof OrbotoApiError) || err.status !== 404)
|
|
@@ -49,15 +61,6 @@ export function makeClaimHandler(client) {
|
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
|
-
if (!alreadyAssigned) {
|
|
53
|
-
try {
|
|
54
|
-
await client.post(`/projects/${current.projectId}/tickets/${current.id}/assignees/${me.id}`, {});
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
if (!(err instanceof OrbotoApiError) || err.status !== 409)
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
64
|
let finalTicket = current;
|
|
62
65
|
if (currentCategory !== 'in_progress') {
|
|
63
66
|
finalTicket = await client.patch(`/projects/${current.projectId}/tickets/${current.id}`, { status: CATEGORY_TO_LEGACY.in_progress });
|