@orboto/mcp 0.208.0 → 0.209.1

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/server.js CHANGED
@@ -14,6 +14,7 @@ import { aiStatusToolConfig, makeAiStatusHandler } from './tools/ai-status.js';
14
14
  import { draftCustomerReplyToolConfig, makeDraftCustomerReplyHandler } from './tools/customer-draft.js';
15
15
  import { embeddingStatusToolConfig, makeEmbeddingStatusHandler } from './tools/embedding-status.js';
16
16
  import { aiUsageToolConfig, makeAiUsageHandler } from './tools/ai-usage.js';
17
+ import { secretScanToolConfig, makeSecretScanHandler } from './tools/secret-scan.js';
17
18
  import { sessionStartToolConfig, makeSessionStartHandler } from './tools/session-start.js';
18
19
  import { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS } from './inbox-channel.js';
19
20
  import { loadRequiredRules } from './required-rules.js';
@@ -195,6 +196,7 @@ export async function buildOrbotoMcpServer(opts) {
195
196
  reg('orboto_draft_customer_reply', draftCustomerReplyToolConfig, makeDraftCustomerReplyHandler(client));
196
197
  reg('orboto_embedding_status', embeddingStatusToolConfig, makeEmbeddingStatusHandler(client));
197
198
  reg('orboto_ai_usage', aiUsageToolConfig, makeAiUsageHandler(client));
199
+ reg('orboto_secret_scan', secretScanToolConfig, makeSecretScanHandler(client));
198
200
  reg('orboto_session_start', sessionStartToolConfig, makeSessionStartHandler(client, { channel: opts.channel }));
199
201
  reg('orboto_response_expand', responseExpandToolConfig, makeResponseExpandHandler());
200
202
  reg('orboto_help', helpToolConfig, makeHelpHandler());
@@ -55,7 +55,7 @@ export function makeApproveTranslationHandler(client) {
55
55
  }
56
56
  export const revertTranslationToolConfig = {
57
57
  title: 'Revert an auto-translated ticket back to the original',
58
- description: 'Restore the pre-translation title + description from the auto-translated audit comment and clear the auto-translate marker + any prior approval. Use when the AI got the translation wrong and the operator wants to start over. 409 if the ticket was never auto-translated. Requires `admin:translation_review`.',
58
+ description: 'Restore the pre-translation title + description from the audit comment and clear the auto-translate marker + prior approval. Use when the AI got the translation wrong and the operator wants to start over. 409 if the ticket was never auto-translated. Requires `admin:translation_review`.',
59
59
  inputSchema: z.object({
60
60
  ticketId: z.string().uuid().describe('UUID of the ticket to revert.'),
61
61
  }).shape,
@@ -42,7 +42,7 @@ export function makeAgentHeartbeatHandler(client) {
42
42
  }
43
43
  export const agentPresenceToolConfig = {
44
44
  title: 'Workspace agent presence',
45
- 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. Each row exposes `userId`, `userEmail`, the agent runtime (`clientInfo.name`), declared `capabilities`, current `status`, the ticket it works on, and `reconnects` survived under the same id and scope. Check it before dispatching a sub-task so you do not 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.',
45
+ description: 'Returns all active orboto agent instances: lane workers/reviewers, inbox and MCP connections, 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. Each row exposes `userId`, `userEmail`, the agent runtime (`clientInfo.name`), declared `capabilities`, current `status`, the ticket it works on, and `reconnects` survived under the same id and scope. Check it before dispatching a sub-task so you do not 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.',
46
46
  inputSchema: z.object({
47
47
  projectKey: z.string().min(1).max(64).optional().describe('Keep only rows whose projects contain this project key (case-insensitive).'),
48
48
  kind: AgentInventoryKindSchema.optional().describe('Keep only rows classified as agent or human by their credential.'),
@@ -140,7 +140,7 @@ export function makeBulkPatchTicketsHandler(client) {
140
140
  }
141
141
  export const bulkMoveTicketsToolConfig = {
142
142
  title: 'Move many tickets to a status category',
143
- description: 'Move every ticket in `ticketKeys` (or the tickets `query` selects) to the same status category (todo / in_progress / in_review / done / wont_fix). Each ticket lands on its own project\'s first status with that category. Returns the per-ticket outcome.',
143
+ description: 'Move every ticket in `ticketKeys` (or `query` selects) to the same status category (todo/in_progress/in_review/done/wont_fix). Each ticket lands on its own project\'s first status with that category. Returns the per-ticket outcome.',
144
144
  inputSchema: z.object({
145
145
  ticketKeys: TICKET_KEY_ARRAY,
146
146
  query: QUERY_SOURCE,
@@ -173,7 +173,7 @@ export function makeBulkMoveTicketsHandler(client) {
173
173
  }
174
174
  export const bulkCloseTicketsToolConfig = {
175
175
  title: 'Close many tickets (optionally with a shared comment)',
176
- description: 'For every ticket in `ticketKeys` (or the tickets `query` selects): optionally post the same `comment` first, then move it to `done`. Mirrors `orboto.mjs bulk-close`. Comment-first ordering means the close note lands in the audit trail even if the status PATCH 403s.',
176
+ description: 'For every ticket in `ticketKeys` (or `query` selects): optionally post the same `comment` first, then move to `done`. Mirrors `orboto.mjs bulk-close`. Comment-first ordering means the close note lands in the audit trail even if the status PATCH 403s.',
177
177
  inputSchema: z.object({
178
178
  ticketKeys: TICKET_KEY_ARRAY,
179
179
  query: QUERY_SOURCE,
@@ -261,7 +261,7 @@ async function makeAssigneeResolver(client, email) {
261
261
  }
262
262
  export const bulkAssignTicketsToolConfig = {
263
263
  title: 'Assign the same user to many tickets',
264
- description: 'POST `assigneeEmail` as an additional assignee on every ticket in `ticketKeys` (or the tickets `query` selects). Multi-assignee is supported - this adds, it does not replace. Idempotent: a 409 (already assigned) counts as success.',
264
+ description: 'POST `assigneeEmail` as an additional assignee on every ticket in `ticketKeys` (or `query` selects). Multi-assignee: this adds, never replaces. Idempotent: a 409 (already assigned) counts as success.',
265
265
  inputSchema: z.object({
266
266
  ticketKeys: TICKET_KEY_ARRAY,
267
267
  query: QUERY_SOURCE,
@@ -304,7 +304,7 @@ export function makeBulkAssignTicketsHandler(client) {
304
304
  }
305
305
  export const bulkUnassignTicketsToolConfig = {
306
306
  title: 'Unassign the same user from many tickets',
307
- description: 'DELETE `assigneeEmail` from every ticket in `ticketKeys` (or the tickets `query` selects). Idempotent: a 404 (was not assigned) counts as success.',
307
+ description: 'DELETE `assigneeEmail` from every ticket in `ticketKeys` (or `query` selects). Idempotent: a 404 (was not assigned) counts as success.',
308
308
  inputSchema: z.object({
309
309
  ticketKeys: TICKET_KEY_ARRAY,
310
310
  query: QUERY_SOURCE,
@@ -37,7 +37,7 @@ function isImage(mimetype) {
37
37
  }
38
38
  export const uploadDocAttachmentToolConfig = {
39
39
  title: 'Upload an attachment to a doc page',
40
- description: 'Upload a file as an attachment on a wiki doc and return its Markdown image (or link) line + the stable download URL. Bytes come in as base64 (`contentBase64`) so this works without local FS access on the agent side. Set `embed=true` to also PATCH the doc body to append the Markdown line - useful for dropping a screenshot into a page in one call. MIME / extension policy is enforced server-side: blocked types (HTML, SVG, executables) surface a 415.',
40
+ description: 'Upload a file as a doc attachment: returns its Markdown image (or link) line + a stable download URL. Bytes come in as base64 (`contentBase64`) so this works without local FS access on the agent side. Set `embed=true` to also PATCH the doc body to append the Markdown line - useful for dropping a screenshot into a page in one call. MIME / extension policy is enforced server-side: blocked types (HTML, SVG, executables) surface a 415.',
41
41
  inputSchema: z.object({
42
42
  docId: z.string().min(1).describe('Doc UUID or human-readable doc key (ORB-D12 / DOC-5).'),
43
43
  filename: z.string().min(1).describe('Display filename, e.g. "architecture-diagram.png".'),
@@ -167,7 +167,7 @@ export function makeEditDocHandler(client) {
167
167
  }
168
168
  export const editDocSectionToolConfig = {
169
169
  title: 'Edit a doc section by heading path (context-efficient)',
170
- description: 'Change a doc by markdown-heading-addressed section operations - ship only the new section content, not the whole doc. A section is addressed by its heading path (e.g. ["Setup","Credentials"] = a "Credentials" heading nested under "Setup"). Heading matching is EXACT + case-sensitive; the leading path elements must be an ordered ancestor chain (not necessarily immediate parents). op=replace swaps the section body, op=append adds to its end, op=insertAfter inserts directly below the heading line (the heading line itself is never removed). A path that matches zero or multiple headings returns a 409 you can act on (narrow the path to disambiguate). Prefer this over orboto_update_doc for section-scoped rewrites. baseRevisionId gives optimistic concurrency. Returns a short context window per op.',
170
+ description: 'Change a doc by heading-addressed section ops - ship only the new section content, not the whole doc. A section is addressed by its heading path (e.g. ["Setup","Credentials"] = a "Credentials" heading nested under "Setup"). Heading matching is EXACT + case-sensitive; the leading path elements must be an ordered ancestor chain (not necessarily immediate parents). op=replace swaps the section body, op=append adds to its end, op=insertAfter inserts directly below the heading line (the heading line itself is never removed). A path that matches zero or multiple headings returns a 409 you can act on (narrow the path to disambiguate). Prefer this over orboto_update_doc for section-scoped rewrites. baseRevisionId gives optimistic concurrency. Returns a short context window per op.',
171
171
  inputSchema: z.object({
172
172
  docId: z.string().min(1).describe('Doc UUID or human-readable key (ORB-D12 / DOC-5). Find via orboto_search_docs.'),
173
173
  sectionOps: z.array(z.object({
@@ -27,7 +27,7 @@ export function makeExportDocMdHandler(client) {
27
27
  }
28
28
  export const exportDocPdfToolConfig = {
29
29
  title: 'Export a doc page as PDF',
30
- description: 'Render the doc page to PDF via the workspace\'s PdfService (Puppeteer-backed) and return the bytes as a base64 MCP resource attachment. Requires the PDF engine to be configured - deployments without Chromium / Puppeteer return 503 (errors.pdf.engine_unavailable) which surfaces as an OrbotoApiError.',
30
+ description: 'Render the doc page to PDF (Puppeteer-backed PdfService) and return the bytes as a base64 MCP resource attachment. Requires the PDF engine to be configured - deployments without Chromium / Puppeteer return 503 (errors.pdf.engine_unavailable) which surfaces as an OrbotoApiError.',
31
31
  inputSchema: z.object({
32
32
  docId: z.string().min(1).describe('Doc UUID or human-readable doc key (ORB-D12 / DOC-5).'),
33
33
  }).shape,
@@ -216,7 +216,7 @@ export function makeDeleteDocSpaceHandler(client) {
216
216
  }
217
217
  export const listDocsInSpaceToolConfig = {
218
218
  title: 'List docs inside a space',
219
- description: 'Return the flat list of doc pages in a space - each carries `parentDocId` so the caller can reconstruct the tree client-side. Use this when an agent needs to find a doc by title rather than asking for a UUID first. Pair with orboto_get_doc to read individual page bodies.',
219
+ description: 'Return the flat list of doc pages in a space - each carries `parentDocId` to reconstruct the tree client-side. Use this when an agent needs to find a doc by title rather than asking for a UUID first. Pair with orboto_get_doc to read individual page bodies.',
220
220
  inputSchema: z.object({
221
221
  spaceId: z.string().min(1).describe('Space key (e.g. ORB-S1), name, or UUID. Discover via orboto_list_doc_spaces.'),
222
222
  }).shape,
@@ -436,7 +436,7 @@ export function makeDuplicateDocSpaceHandler(client) {
436
436
  }
437
437
  export const resolveDocSmartLinksToolConfig = {
438
438
  title: 'Batch-resolve smart-link references to display metadata',
439
- description: 'Resolve `[[doc:UUID]]` / `[[ticket:UUID]]` / `[[milestone:UUID]]` / `[[project:UUID]]` / commit-hash references to their current title + URL. Visibility-filtered - items the caller is not allowed to see come back as missing (the rendering frontend falls back to the literal label in that case). Useful when reading a doc body that contains many tokens and you want to display them with current titles in one round-trip rather than N+1 get-* calls. Max 200 items per call.',
439
+ description: 'Resolve `[[doc:UUID]]`/`[[ticket:UUID]]`/`[[milestone:UUID]]`/`[[project:UUID]]`/commit-hash references to their title + URL. Visibility-filtered - items the caller is not allowed to see come back as missing (the rendering frontend falls back to the literal label in that case). Useful when reading a doc body that contains many tokens and you want to display them with current titles in one round-trip rather than N+1 get-* calls. Max 200 items per call.',
440
440
  inputSchema: z.object({
441
441
  items: z.array(z.object({
442
442
  type: z.enum(['doc', 'ticket', 'milestone', 'project', 'commit']),
@@ -4,7 +4,7 @@
4
4
  import { z } from 'zod';
5
5
  export const whoamiToolConfig = {
6
6
  title: 'Show the authenticated MCP principal',
7
- description: 'Return the authenticated user record `{id, email, fullName, isBot, workspaceLocale}` corresponding to the API key this MCP server is running with. Useful for debugging which credential / bot identity is in use when multiple MCP configs are wired to different `orb_*` keys. **`workspaceLocale` (ORB-989)** is the language the workspace expects tickets / comments / docs in - check it before a mass-create so you write in the right language from the start instead of relying on the after-the-fact language-mismatch warning.',
7
+ description: 'Return the authenticated user record `{id, email, fullName, isBot, workspaceLocale}` for the API key this server runs with. Useful for debugging which credential / bot identity is in use when multiple MCP configs are wired to different `orb_*` keys. **`workspaceLocale` (ORB-989)** is the language the workspace expects tickets / comments / docs in - check it before a mass-create so you write in the right language from the start instead of relying on the after-the-fact language-mismatch warning.',
8
8
  inputSchema: z.object({}).shape,
9
9
  outputSchema: z.object({
10
10
  id: z.string(),
@@ -11,7 +11,7 @@
11
11
  import { z } from 'zod';
12
12
  export const listGitAppInstallationsToolConfig = {
13
13
  title: 'List GitHub App installations',
14
- description: 'Every GitHub App installation orboto knows about, across projects (super-admin): the org/user, install time and whether it is suspended.',
14
+ description: 'Every GitHub App installation orboto knows, across projects (super-admin): org/user, install time, suspended state.',
15
15
  inputSchema: z.object({}).shape,
16
16
  annotations: { readOnlyHint: true, idempotentHint: true },
17
17
  };
@@ -53,7 +53,7 @@ function summariseFact(f) {
53
53
  }
54
54
  export const primerFactListToolConfig = {
55
55
  title: 'List structured project primer facts',
56
- description: 'List structured project facts that feed the AI primer (tech stack, conventions, deployment, architecture, integrations, gotchas, commands). Use this to discover what the project has already documented before adding a new observation. Workspace-wide facts (applying to every project) are merged in by default; pass includeWorkspace=false to see only project-scoped rows.',
56
+ description: 'List structured project facts feeding the AI primer (stack, conventions, deployment, architecture, integrations, gotchas, commands). Use this to discover what the project has already documented before adding a new observation. Workspace-wide facts (applying to every project) are merged in by default; pass includeWorkspace=false to see only project-scoped rows.',
57
57
  inputSchema: z.object({
58
58
  projectKey: z.string().min(1).describe('Project key, e.g. "ORB". Case-insensitive.'),
59
59
  category: PrimerFactCategoryEnum.optional().describe('Filter by category. Omit for all categories.'),
@@ -60,7 +60,7 @@ export function makeRaciHandler(client) {
60
60
  }
61
61
  export const setRaciToolConfig = {
62
62
  title: 'Set a RACI role',
63
- description: "Set a person's RACI role on a ticket: R (Responsible), A (Accountable, max one per ticket), C (Consulted), or I (Informed). RACI is OPT-IN per project: only use this when the project has RACI enabled (`raciEnabled` true) - do NOT suggest or set RACI on projects that haven't opted in. Resolves the user by email within the ticket's project. Requires ticket:manage_raci. A second Accountable is rejected with the current holder's name.",
63
+ description: "Set a person's RACI role: R (Responsible), A (Accountable, max one per ticket), C (Consulted), I (Informed). RACI is OPT-IN per project: only use this when the project has RACI enabled (`raciEnabled` true) - do NOT suggest or set RACI on projects that haven't opted in. Resolves the user by email within the ticket's project. Requires ticket:manage_raci. A second Accountable is rejected with the current holder's name.",
64
64
  inputSchema: z.object({
65
65
  ticketKey: z.string().min(3).describe('Ticket key (e.g. "ORB-42").'),
66
66
  userEmail: z.string().email().describe('Email of a project member.'),
@@ -52,7 +52,7 @@ export function makeReviewPolicyCheckHandler(client) {
52
52
  }
53
53
  export const reviewApprovalRecordToolConfig = {
54
54
  title: 'Record a review decision against a diff fingerprint',
55
- description: 'Record your review verdict (approve/reject) against the complete version-prefixed fingerprint from orboto_review_fingerprint. A current-algorithm approval is reusable only for identical submitted diff text; even whitespace or file-order changes require a fresh review. Legacy/unversioned records remain history only. This advisory review ledger does not itself enforce ticket closure. Requires ticket:record_review_approval.',
55
+ description: 'Record your review verdict (approve/reject) against the version-prefixed fingerprint from orboto_review_fingerprint. A current-algorithm approval is reusable only for identical submitted diff text; even whitespace or file-order changes require a fresh review. Legacy/unversioned records remain history only. This advisory review ledger does not itself enforce ticket closure. Requires ticket:record_review_approval.',
56
56
  inputSchema: z.object({
57
57
  ticketKey: z.string().min(3).describe('Ticket key, e.g. ORB-42.'),
58
58
  fingerprint: z.string().min(8).describe('From orboto_review_fingerprint.'),
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ORB-2229 - `orboto_secret_scan`, the admin secret scan.
3
+ */
4
+ import { z } from 'zod';
5
+ export const secretScanToolConfig = {
6
+ title: 'Secret scan',
7
+ description: 'Admin sweep for stored secret-shaped values (ORB-2229); see skill reference admin.md "Secret scan".',
8
+ inputSchema: z.object({
9
+ redact: z.boolean().optional().describe('Replace found values, purge history (default false).'),
10
+ limit: z.number().int().min(1).max(1000).optional().describe('Max hits (default 200).'),
11
+ }).shape,
12
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
13
+ };
14
+ export function makeSecretScanHandler(client) {
15
+ return async (args) => {
16
+ const report = await client.post('/admin/content/secret-scan', {
17
+ redact: args.redact ?? false,
18
+ limit: args.limit ?? 200,
19
+ });
20
+ const lines = [];
21
+ const counts = Object.entries(report.scanned).map(([k, n]) => `${k} ${n}`).join(', ');
22
+ lines.push(`Secret scan at ${report.scannedAt} - scanned ${counts}`);
23
+ if (report.hits.length === 0) {
24
+ lines.push('No secret-shaped value found.');
25
+ }
26
+ else {
27
+ for (const hit of report.hits) {
28
+ const where = [hit.label, hit.projectKey ? `(${hit.projectKey})` : null, hit.spaceVisibility ? `[space: ${hit.spaceVisibility}]` : null].filter(Boolean).join(' ');
29
+ const state = hit.redacted ? ' - redacted' : '';
30
+ lines.push(`${hit.entity} ${where}${state}`);
31
+ for (const f of hit.findings) {
32
+ lines.push(` ${f.class} in ${hit.field} at line ${f.line}, column ${f.column} (${f.preview}, ${f.length} chars)`);
33
+ }
34
+ }
35
+ lines.push(report.redacted > 0
36
+ ? `Redacted ${report.redacted} rows; rotate every credential listed above.`
37
+ : `${report.hits.length} hits; rotate every credential listed above, then run with redact: true.`);
38
+ }
39
+ return {
40
+ content: [{ type: 'text', text: lines.join('\n') }],
41
+ structuredContent: {
42
+ scannedAt: report.scannedAt,
43
+ scanned: report.scanned,
44
+ hitCount: report.hits.length,
45
+ redactedCount: report.redacted,
46
+ hits: report.hits.map((h) => ({
47
+ entity: h.entity, id: h.id, label: h.label, field: h.field,
48
+ projectKey: h.projectKey, spaceVisibility: h.spaceVisibility, webUrl: h.webUrl,
49
+ classes: h.findings.map((f) => f.class), redacted: h.redacted,
50
+ })),
51
+ },
52
+ };
53
+ };
54
+ }
@@ -14,7 +14,7 @@ function isImage(mimetype) {
14
14
  }
15
15
  export const listTicketAttachmentsToolConfig = {
16
16
  title: 'List attachments on a ticket',
17
- description: 'Return a ticket\'s attachments newest-first, with each attachment\'s id, filename, content type, size, and stable download URL. Use the returned id with orboto_get_attachment to actually view an image or fetch the bytes. Empty list = no attachments. Input is the ticket key like "ACME-42".',
17
+ description: 'Return a ticket\'s attachments newest-first: id, filename, content type, size, stable download URL. Use the returned id with orboto_get_attachment to actually view an image or fetch the bytes. Empty list = no attachments. Input is the ticket key like "ACME-42".',
18
18
  inputSchema: z.object({
19
19
  ticketKey: z.string().min(3).describe('Ticket key like "ACME-42".'),
20
20
  }).shape,
@@ -57,7 +57,7 @@ export function makeListTicketAttachmentsHandler(client) {
57
57
  }
58
58
  export const getAttachmentToolConfig = {
59
59
  title: 'View or fetch an attachment\'s bytes',
60
- description: 'Fetch a single attachment by its id (from orboto_list_ticket_attachments, orboto_get_ticket, or orboto_list_doc_attachments) and return its content. For an image, the model receives an image content block so it actually SEES the screenshot, plus a text line with filename and size. For a non-image, the model gets a text block with metadata and, for small files, the base64 content; large binaries point at the skill\'s download-attachment shortcut. Works for ticket, doc, and comment attachments (the id is global). Enforces the same project/space access check as listing the attachment.',
60
+ description: 'Fetch an attachment by id (from orboto_list_ticket_attachments or orboto_get_ticket) and return its content. For an image, the model receives an image content block so it actually SEES the screenshot, plus a text line with filename and size. For a non-image, the model gets a text block with metadata and, for small files, the base64 content; large binaries point at the skill\'s download-attachment shortcut. Works for ticket, doc, and comment attachments (the id is global). Enforces the same project/space access check as listing the attachment.',
61
61
  inputSchema: z.object({
62
62
  attachmentId: z.string().uuid().describe('Attachment UUID from a list/get call.'),
63
63
  }).shape,
@@ -403,7 +403,7 @@ export function makeCloseTicketHandler(client) {
403
403
  }
404
404
  export const deleteTicketToolConfig = {
405
405
  title: 'Permanently delete a ticket',
406
- description: 'DESTRUCTIVE, IRREVERSIBLE hard-delete of a ticket (by key): the row and its history are gone, and a `ticket.deleted` event + webhook fire. Strongly prefer moving the ticket to `wont_fix` (orboto_move_ticket) or closing it instead - wont_fix keeps the history and analytics intact. Only hard-delete a ticket that should truly never have existed (accidental duplicate, spam). Caller must have `ticket:delete`.',
406
+ description: 'DESTRUCTIVE, IRREVERSIBLE hard-delete of a ticket (by key): row and history gone, a `ticket.deleted` event + webhook fire. Strongly prefer moving the ticket to `wont_fix` (orboto_move_ticket) or closing it instead - wont_fix keeps the history and analytics intact. Only hard-delete a ticket that should truly never have existed (accidental duplicate, spam). Caller must have `ticket:delete`.',
407
407
  inputSchema: z.object({
408
408
  ticketKey: z.string().min(3),
409
409
  }).shape,
@@ -719,7 +719,7 @@ export function makeRemoveTicketDependencyHandler(client) {
719
719
  }
720
720
  export const listTicketDependenciesToolConfig = {
721
721
  title: 'List a ticket\'s dependencies',
722
- description: 'Show both directions of the dependency graph for a ticket: `blockedBy` (tickets that must finish first) and `blocks` (tickets waiting on this one). ORB-1614: an edge to a ticket in another project you cannot read comes back as an opaque "external dependency" entry (no title/key/status - just whether it is still open).',
722
+ description: 'Show both directions of a ticket\'s dependency graph: `blockedBy` (must finish first) and `blocks` (waiting on this one). ORB-1614: an edge to a ticket in another project you cannot read comes back as an opaque "external dependency" entry (no title/key/status - just whether it is still open).',
723
723
  inputSchema: z.object({
724
724
  ticketKey: z.string().min(3),
725
725
  }).shape,
@@ -165,7 +165,7 @@ export function makeListTimeEntriesHandler(client) {
165
165
  }
166
166
  export const editTimeEntryToolConfig = {
167
167
  title: 'Edit / correct a time entry',
168
- description: 'Correct a time entry - fix an over-tracked duration, edit its note, change its date, or re-target it to another ticket. Use THIS (not orboto_log_time) to fix a wrong entry: log_time is additive and only makes the total worse. Get the entryId from orboto_list_time_entries (or an orboto_log_time response). Blocked if the entry is locked by an approved timesheet.',
168
+ description: 'Correct a time entry: fix a duration, edit its note, change its date, or re-target it to another ticket. Use THIS (not orboto_log_time) to fix a wrong entry: log_time is additive and only makes the total worse. Get the entryId from orboto_list_time_entries (or an orboto_log_time response). Blocked if the entry is locked by an approved timesheet.',
169
169
  inputSchema: z.object({
170
170
  ticketKey: z.string().min(3).describe('The ticket the entry currently belongs to.'),
171
171
  entryId: z.string().uuid(),
@@ -71,7 +71,7 @@ export function makeWikiLintHandler(client) {
71
71
  }
72
72
  export const wikiPlanUpdateToolConfig = {
73
73
  title: 'Plan a wiki edit (dry-run, no writes)',
74
- description: 'Turn a natural-language instruction into a concrete set of page operations (create / patch / append) WITHOUT writing anything. Returns a planId valid for 15 minutes plus the proposed ops. Review the ops, then call orboto_wiki_apply_plan to commit. Wraps POST /spaces/:id/docs/plan-update.',
74
+ description: 'Turn a natural-language instruction into page operations (create/patch/append) WITHOUT writing anything. Returns a planId valid for 15 minutes plus the proposed ops. Review the ops, then call orboto_wiki_apply_plan to commit. Wraps POST /spaces/:id/docs/plan-update.',
75
75
  inputSchema: z.object({
76
76
  spaceId: z.string().uuid().describe('The wiki space to edit.'),
77
77
  instruction: z.string().min(1).max(4000).describe('What to change, in plain language.'),
@@ -23,7 +23,7 @@ function landedIdleLines(rows) {
23
23
  }
24
24
  export const workNextToolConfig = {
25
25
  title: 'Pull and reserve the next ready ticket (worker-pool dispatch)',
26
- description: 'ORB-1613 - the pull side of low-management dispatch, sibling of orboto_work_start on the "I already know which ticket" side. Picks the highest-priority ticket in a project that is READY FOR THE REQUESTED ROLE (default role implementation pulls TODO tickets; `role: "review"` pulls tickets in the in_review status category instead, and NEVER offers a ticket the caller itself implemented - the review lane primitive, ORB-1777), unblocked (every dependency closed), not already leased under the requested role, and not blocked by a conflicting resourceClaim - then reserves it with the EXACT same guarantees as orboto_work_start (atomic lease acquire + the full context bundle: rules ack, primer, ticket, checklists, dependencies, git health, siblings) in the same response. Priority then ticket number, deterministic - never a coin flip on ties. Two workers calling this concurrently never receive the SAME ticket: the underlying reservation is the identical partial-unique-index INSERT orboto_work_start uses, just walked across an ordered candidate list - a collision just advances to the next candidate. Epics are never returned (they are containers, not directly implementable). When nothing is ready, the response is a STRUCTURED result (`reserved: null`) with a `reason` (`none-matching` = no todo tickets at all; `all-blocked` = candidates exist but all have open dependencies; `all-leased` = ready candidates exist but are all currently leased or claim-conflicted; `autonomy_paused` = ORB-1774, this agent identity or the whole workspace has autonomous pulls paused by an operator - idle and wait for an operator/notify wakeup instead of retrying) and, ONLY when derivable from an actual active lease, a `retryAfterSeconds` backoff hint (`earliestFreeAt` null and `retryAfterSeconds` null means no signal exists - never a fabricated constant). This is never an error - a worker pool should back off on the hint rather than poll hot. Prefer this over orboto_work_start whenever the caller does not care WHICH ticket it gets, only that it gets the best available one right now. Every response - reserved, empty or paused - also carries `landedIdle`: YOUR OWN tickets in this project that are still in_progress with a linked commit and no activity for days (ORB-1799). Those are finished-looking work nobody handed to the review lane; move them on instead of re-implementing them.',
26
+ description: 'ORB-1613 - the pull side of low-management dispatch, sibling of orboto_work_start for "any ticket will do". Picks the highest-priority ticket in a project that is READY FOR THE REQUESTED ROLE (default role implementation pulls TODO tickets; `role: "review"` pulls tickets in the in_review status category instead, and NEVER offers a ticket the caller itself implemented - the review lane primitive, ORB-1777), unblocked (every dependency closed), not already leased under the requested role, and not blocked by a conflicting resourceClaim - then reserves it with the EXACT same guarantees as orboto_work_start (atomic lease acquire + the full context bundle: rules ack, primer, ticket, checklists, dependencies, git health, siblings) in the same response. Priority then ticket number, deterministic - never a coin flip on ties. Two workers calling this concurrently never receive the SAME ticket: the underlying reservation is the identical partial-unique-index INSERT orboto_work_start uses, just walked across an ordered candidate list - a collision just advances to the next candidate. Epics are never returned (they are containers, not directly implementable). When nothing is ready, the response is a STRUCTURED result (`reserved: null`) with a `reason` (`none-matching` = no todo tickets at all; `all-blocked` = candidates exist but all have open dependencies; `all-leased` = ready candidates exist but are all currently leased or claim-conflicted; `autonomy_paused` = ORB-1774, this agent identity or the whole workspace has autonomous pulls paused by an operator - idle and wait for an operator/notify wakeup instead of retrying) and, ONLY when derivable from an actual active lease, a `retryAfterSeconds` backoff hint (`earliestFreeAt` null and `retryAfterSeconds` null means no signal exists - never a fabricated constant). This is never an error - a worker pool should back off on the hint rather than poll hot. Prefer this over orboto_work_start whenever the caller does not care WHICH ticket it gets, only that it gets the best available one right now. Every response - reserved, empty or paused - also carries `landedIdle`: YOUR OWN tickets in this project that are still in_progress with a linked commit and no activity for days (ORB-1799). Those are finished-looking work nobody handed to the review lane; move them on instead of re-implementing them.',
27
27
  inputSchema: z.object({
28
28
  projectKey: z.string().min(1).describe('Project key (e.g. "ACME") or UUID.'),
29
29
  agentTag: z.string().min(1).max(64).optional()
@@ -456,7 +456,7 @@ export function makeWorkSessionsHandler(client) {
456
456
  }
457
457
  export const workSessionClaimsAddToolConfig = {
458
458
  title: 'Add resource claims to a live work session',
459
- description: 'ORB-1610 - declare more resource claims on a session you already hold (from orboto_work_session_start), without touching the ticket lease or timer. Use this when you did not know the files/resources you would touch at start time, or need to widen scope mid-task. Same conflict rule as start: `write` claims conflict with any OVERLAPPING active write claim workspace-wide; `read` claims never conflict. Default `onConflict: "reject"` fails the WHOLE call (nothing is added) and names every conflicting holder; `queue` accepts the conflicting ones as `state: "waiting"`.',
459
+ description: 'ORB-1610 - declare more resource claims on a session you hold, without touching the lease or timer. Use this when you did not know the files/resources you would touch at start time, or need to widen scope mid-task. Same conflict rule as start: `write` claims conflict with any OVERLAPPING active write claim workspace-wide; `read` claims never conflict. Default `onConflict: "reject"` fails the WHOLE call (nothing is added) and names every conflicting holder; `queue` accepts the conflicting ones as `state: "waiting"`.',
460
460
  inputSchema: z.object({
461
461
  sessionId: z.string().uuid().describe('The id returned by orboto_work_session_start.'),
462
462
  claims: z.array(z.object(ResourceClaimShape)).min(1).max(50)
@@ -501,7 +501,7 @@ export function makeWorkSessionClaimsAddHandler(client) {
501
501
  }
502
502
  export const workSessionClaimsReleaseToolConfig = {
503
503
  title: 'Release resource claims from a live work session',
504
- description: 'ORB-1610 - drop specific resource claims (or all of them) from a session you hold, WITHOUT finishing the session or touching the ticket lease/timer. Releasing a granted write claim immediately runs a grant pass, so the earliest queued waiter for that resource is promoted as part of this call - useful once you know you are done touching a subtree but are not done with the ticket. Omit `claims` to release everything the session holds.',
504
+ description: 'ORB-1610 - drop resource claims (or all) from a session you hold, WITHOUT finishing it or touching the ticket lease/timer. Releasing a granted write claim immediately runs a grant pass, so the earliest queued waiter for that resource is promoted as part of this call - useful once you know you are done touching a subtree but are not done with the ticket. Omit `claims` to release everything the session holds.',
505
505
  inputSchema: z.object({
506
506
  sessionId: z.string().uuid().describe('The id returned by orboto_work_session_start.'),
507
507
  claims: z.array(z.object({ kind: z.enum(['path', 'named']), value: z.string().min(1).max(500) })).optional()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orboto/mcp",
3
- "version": "0.208.0",
3
+ "version": "0.209.1",
4
4
  "description": "Model Context Protocol server for orboto - exposes tickets, projects, milestones, time, docs, primer-facts, alerts and absences as MCP tools for Claude Desktop / Cursor / Copilot Chat / any MCP-aware AI client.",
5
5
  "license": "MIT",
6
6
  "type": "module",