@orboto/mcp 0.207.0 → 0.209.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/server.js +7 -1
- package/dist/tools/admin-translations.js +1 -1
- package/dist/tools/agent-coordination.js +1 -1
- package/dist/tools/agent-drift.js +1 -1
- package/dist/tools/agent-instructions.js +2 -2
- package/dist/tools/agent-messages.js +1 -1
- package/dist/tools/agent-wakes.js +1 -1
- package/dist/tools/backup.js +1 -1
- package/dist/tools/bulk-create.js +2 -2
- package/dist/tools/bulk-writes.js +4 -4
- package/dist/tools/check-similar.js +1 -1
- package/dist/tools/customer-draft.js +1 -1
- package/dist/tools/doc-attachments.js +1 -1
- package/dist/tools/doc-edits.js +1 -1
- package/dist/tools/doc-export.js +1 -1
- package/dist/tools/docs.js +2 -2
- package/dist/tools/get-project-primer.js +1 -1
- package/dist/tools/help.js +1 -1
- package/dist/tools/identity.js +1 -1
- package/dist/tools/knowledge.js +35 -0
- package/dist/tools/knowledge.test.js +74 -0
- package/dist/tools/list-git-app-installations.js +1 -1
- package/dist/tools/primer-facts.js +1 -1
- package/dist/tools/raci.js +1 -1
- package/dist/tools/review-policy.js +1 -1
- package/dist/tools/secret-scan.js +54 -0
- package/dist/tools/sentry.js +1 -1
- package/dist/tools/session-start.js +34 -2
- package/dist/tools/ticket-attachments.js +2 -2
- package/dist/tools/ticket-writes.js +2 -2
- package/dist/tools/time-writes.js +1 -1
- package/dist/tools/wiki.js +29 -4
- package/dist/tools/work-next.js +1 -1
- package/dist/tools/work-sessions.js +3 -3
- package/package.json +1 -1
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';
|
|
@@ -28,6 +29,7 @@ import { agentMessageWorkToolConfig, makeAgentMessageWorkHandler } from './tools
|
|
|
28
29
|
import { agentMessagesToolConfig, makeAgentMessagesHandler } from './tools/agent-messages.js';
|
|
29
30
|
import { agentWakesToolConfig, makeAgentWakesHandler } from './tools/agent-wakes.js';
|
|
30
31
|
import { sessionCheckToolConfig, makeSessionCheckHandler } from './tools/session-check.js';
|
|
32
|
+
import { knowledgeAskToolConfig, makeKnowledgeAskHandler, knowledgeAddToolConfig, makeKnowledgeAddHandler } from './tools/knowledge.js';
|
|
31
33
|
import { listProjectsToolConfig, makeListProjectsHandler } from './tools/list-projects.js';
|
|
32
34
|
import { getProjectToolConfig, makeGetProjectHandler } from './tools/get-project.js';
|
|
33
35
|
import { projectReadinessToolConfig, makeProjectReadinessHandler } from './tools/project-readiness.js';
|
|
@@ -45,7 +47,7 @@ import { queryToolConfig, makeQueryHandler } from './tools/query.js';
|
|
|
45
47
|
import { customerReportToolConfig, makeCustomerReportHandler } from './tools/customer-report.js';
|
|
46
48
|
import { requirementsSpecToolConfig, makeRequirementsSpecHandler } from './tools/requirements-spec.js';
|
|
47
49
|
import { specCheckToolConfig, makeSpecCheckHandler } from './tools/spec-check.js';
|
|
48
|
-
import { wikiIngestUrlToolConfig, makeWikiIngestUrlHandler, wikiAskToolConfig, makeWikiAskHandler, wikiLintToolConfig, makeWikiLintHandler, wikiPlanUpdateToolConfig, makeWikiPlanUpdateHandler, wikiApplyPlanToolConfig, makeWikiApplyPlanHandler, wikiRecordToolConfig, makeWikiRecordHandler, wikiAppendSectionToolConfig, makeWikiAppendSectionHandler, wikiFlagStaleToolConfig, makeWikiFlagStaleHandler, wikiSaveAnswerToolConfig, makeWikiSaveAnswerHandler, } from './tools/wiki.js';
|
|
50
|
+
import { wikiIngestUrlToolConfig, makeWikiIngestUrlHandler, wikiAskToolConfig, makeWikiAskHandler, wikiLintToolConfig, makeWikiLintHandler, wikiPlanUpdateToolConfig, makeWikiPlanUpdateHandler, wikiApplyPlanToolConfig, makeWikiApplyPlanHandler, wikiRecordToolConfig, makeWikiRecordHandler, wikiAppendSectionToolConfig, makeWikiAppendSectionHandler, wikiFlagStaleToolConfig, makeWikiFlagStaleHandler, wikiSaveAnswerToolConfig, makeWikiSaveAnswerHandler, wikiRunsToolConfig, makeWikiRunsHandler } from './tools/wiki.js';
|
|
49
51
|
import { personalFactListToolConfig, makePersonalFactListHandler, personalFactAddToolConfig, makePersonalFactAddHandler, personalFactUpdateToolConfig, makePersonalFactUpdateHandler, personalFactDeleteToolConfig, makePersonalFactDeleteHandler, } from './tools/personal-facts.js';
|
|
50
52
|
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';
|
|
51
53
|
import { searchDocsToolConfig, makeSearchDocsHandler, editDocToolConfig, makeEditDocHandler, editDocSectionToolConfig, makeEditDocSectionHandler, } from './tools/doc-edits.js';
|
|
@@ -194,6 +196,7 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
194
196
|
reg('orboto_draft_customer_reply', draftCustomerReplyToolConfig, makeDraftCustomerReplyHandler(client));
|
|
195
197
|
reg('orboto_embedding_status', embeddingStatusToolConfig, makeEmbeddingStatusHandler(client));
|
|
196
198
|
reg('orboto_ai_usage', aiUsageToolConfig, makeAiUsageHandler(client));
|
|
199
|
+
reg('orboto_secret_scan', secretScanToolConfig, makeSecretScanHandler(client));
|
|
197
200
|
reg('orboto_session_start', sessionStartToolConfig, makeSessionStartHandler(client, { channel: opts.channel }));
|
|
198
201
|
reg('orboto_response_expand', responseExpandToolConfig, makeResponseExpandHandler());
|
|
199
202
|
reg('orboto_help', helpToolConfig, makeHelpHandler());
|
|
@@ -211,6 +214,8 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
211
214
|
reg('orboto_messages', agentMessagesToolConfig, makeAgentMessagesHandler(client));
|
|
212
215
|
reg('orboto_agent_wakes', agentWakesToolConfig, makeAgentWakesHandler(client));
|
|
213
216
|
reg('orboto_session_check', sessionCheckToolConfig, makeSessionCheckHandler(client));
|
|
217
|
+
reg('orboto_knowledge_ask', knowledgeAskToolConfig, makeKnowledgeAskHandler(client));
|
|
218
|
+
reg('orboto_knowledge_add', knowledgeAddToolConfig, makeKnowledgeAddHandler(client));
|
|
214
219
|
reg('orboto_agent_broadcast', agentBroadcastToolConfig, makeAgentBroadcastHandler(client));
|
|
215
220
|
reg('orboto_list_projects', listProjectsToolConfig, makeListProjectsHandler(client));
|
|
216
221
|
reg('orboto_get_project', getProjectToolConfig, makeGetProjectHandler(client));
|
|
@@ -269,6 +274,7 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
269
274
|
reg('orboto_wiki_ingest_url', wikiIngestUrlToolConfig, makeWikiIngestUrlHandler(client));
|
|
270
275
|
reg('orboto_wiki_ask', wikiAskToolConfig, makeWikiAskHandler(client));
|
|
271
276
|
reg('orboto_wiki_lint', wikiLintToolConfig, makeWikiLintHandler(client));
|
|
277
|
+
reg('orboto_wiki_runs', wikiRunsToolConfig, makeWikiRunsHandler(client));
|
|
272
278
|
reg('orboto_wiki_plan_update', wikiPlanUpdateToolConfig, makeWikiPlanUpdateHandler(client));
|
|
273
279
|
reg('orboto_wiki_apply_plan', wikiApplyPlanToolConfig, makeWikiApplyPlanHandler(client));
|
|
274
280
|
reg('orboto_wiki_record', wikiRecordToolConfig, makeWikiRecordHandler(client));
|
|
@@ -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
|
|
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
|
|
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.'),
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
export const listAgentDriftToolConfig = {
|
|
13
13
|
title: 'List agent drift events',
|
|
14
|
-
description: 'List agent drift events
|
|
14
|
+
description: 'List agent drift events newest first, with aggregate metrics. A drift event is a commit with no ticket key and no timer, or a ticket transition without a summary comment. Filter by `userId`, `driftType` (untracked_commit | transition_without_summary), `from`/`to` ISO dates, and `resolved`. The response `enabled` flag is false when the operator has not turned drift detection on. Requires `admin:agent_drift:read`.',
|
|
15
15
|
inputSchema: z.object({
|
|
16
16
|
userId: z.string().uuid().optional().describe('Filter to one user.'),
|
|
17
17
|
driftType: z.enum(['untracked_commit', 'transition_without_summary']).optional(),
|
|
@@ -18,7 +18,7 @@ function renderBlock(b) {
|
|
|
18
18
|
}
|
|
19
19
|
export const listAgentInstructionsToolConfig = {
|
|
20
20
|
title: 'MANAGE the workspace coding-agent rule blocks (admin)',
|
|
21
|
-
description: 'ADMIN
|
|
21
|
+
description: 'ADMIN tool: lists the rule BLOCKS at a scope for editing, not the rules to follow. Scopes: workspace needs admin:ai:read, project needs project:edit, personal is your own; blocks can be edited, toggled and reordered. This is NOT how you read the rules to follow. To LOAD the rules you must follow as an agent, call orboto_session_start instead - it returns the complete assembled rule set.',
|
|
22
22
|
inputSchema: z.object({
|
|
23
23
|
scope: z.enum(['workspace', 'customer', 'project', 'personal']).default('workspace'),
|
|
24
24
|
projectId: z.string().uuid().optional().describe('Required for scope=project.'),
|
|
@@ -72,7 +72,7 @@ export function makeListAgentInstructionsHandler(client) {
|
|
|
72
72
|
}
|
|
73
73
|
export const createAgentInstructionToolConfig = {
|
|
74
74
|
title: 'Add a custom coding-agent rule block',
|
|
75
|
-
description: 'Create a custom rule block at a scope
|
|
75
|
+
description: 'Create a custom rule block at a scope (workspace, customer, project or personal). Workspace reaches every agent (admin:ai:write), customer every project of one customer (customer:write), project one project (project:edit), personal only you. For scope=customer pass customerId; for scope=project pass projectId.'
|
|
76
76
|
+ RULE_WRITING_CONTRACT,
|
|
77
77
|
inputSchema: z.object({
|
|
78
78
|
title: z.string().min(1).max(120),
|
|
@@ -13,7 +13,7 @@ import { mcpInstanceToken } from './shared.js';
|
|
|
13
13
|
const PartySchema = z.object({ userId: z.string(), email: z.string(), sessionId: z.string().nullable(), role: z.string().nullable(), label: z.string() });
|
|
14
14
|
export const agentMessagesToolConfig = {
|
|
15
15
|
title: 'Agent inbox and message work',
|
|
16
|
-
description: 'Your inbox as THIS session sees it
|
|
16
|
+
description: 'Your inbox as THIS session sees it. It holds mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out, all:true shows the whole account (ORB-2136). messageWork carries durable work ownership - claim before executing. ackIds acks: it completes an unclaimed request; claimed work with open steps is refused until messageWork completes or blocks it. dismiss is the third answer: not_mine hides it here and tells the sender, obsolete/duplicate close it for the account (note or duplicateOf required). Never leave mail you read and judged. Reply via orboto_agent_notify with threadId (toSessionRef = from.sessionId reaches that instance).',
|
|
17
17
|
inputSchema: z.object({
|
|
18
18
|
messageWork: z.record(z.unknown()).optional()
|
|
19
19
|
.describe('Work envelope {messageId, mutation, cursor, limit, openOnly}; schema: orboto_api_search.'),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
export const agentWakesToolConfig = {
|
|
7
7
|
title: 'Wake ledger',
|
|
8
|
-
description: '
|
|
8
|
+
description: 'The wake ledger: who woke this account\'s sessions and at what cost. It names the visibility rule that delivered the message (addressed | scope_project | scope_ticket | broadcast | unscoped_session | account_view), what the woken session did with it (handled | not_mine | obsolete | duplicate | expired | pending) and what the wake cost in context tokens. `stats: true` returns delivery precision per woken session and per sender over `hours` (operator read, needs admin:agents:read); `wakes`, `precision` and the median latency count deliveries (`channel` | `follow` | `poll`) only, `backlogSeen` is the separate count of `nudge` rows - a backlog sighting, not a wake. Without `stats` it returns the ledger rows of your own account, newest first. Use it to prove a misrouting sender instead of describing it in prose.',
|
|
9
9
|
inputSchema: z.object({
|
|
10
10
|
stats: z.boolean().default(false).describe('Precision per session and per sender instead of rows.'),
|
|
11
11
|
hours: z.number().int().min(1).max(720).optional().describe('Stats window, default 24.'),
|
package/dist/tools/backup.js
CHANGED
|
@@ -69,7 +69,7 @@ export function makeListBackupsHandler(client) {
|
|
|
69
69
|
}
|
|
70
70
|
export const downloadBackupToolConfig = {
|
|
71
71
|
title: 'Download a stored backup run',
|
|
72
|
-
description: 'Download a stored backup run by run id
|
|
72
|
+
description: 'Download a stored backup run by run id as a base64 resource attachment. The id comes from orboto_list_backups; an encrypted run downloads sealed and needs its key to restore. Requires admin:backup:read.',
|
|
73
73
|
inputSchema: z.object({
|
|
74
74
|
runId: z.string().uuid().describe('Backup run id from orboto_list_backups.'),
|
|
75
75
|
}).shape,
|
|
@@ -22,7 +22,7 @@ const TicketDraftSchema = z.object({
|
|
|
22
22
|
});
|
|
23
23
|
export const bulkCreateTicketsToolConfig = {
|
|
24
24
|
title: 'Create many tickets in one call',
|
|
25
|
-
description: 'Create up to 50 tickets in one call
|
|
25
|
+
description: 'Create up to 50 tickets in one call. It is the right tool whenever you are about to create more than ~3 tickets (milestone planning, epic breakdown, import). Per-draft error isolation: a rejected draft (bad label, language block, hard duplicate-block) is reported in `failed` while the rest are still created. Duplicate detection runs per draft and is reported compactly - one line per flagged draft in `duplicateFlags`; review those before treating them as new work. A call-level `milestone` / `parentTicketKey` applies to every draft unless the draft overrides it. Drafts are created in array order, so a draft can NOT reference a sibling draft as parent - create parents first (or in a first call).',
|
|
26
26
|
inputSchema: z.object({
|
|
27
27
|
projectKey: z.string().min(1).describe('Project key - all drafts land here.'),
|
|
28
28
|
tickets: z.array(TicketDraftSchema).min(1).max(50).describe('The ticket drafts, created in order.'),
|
|
@@ -137,7 +137,7 @@ export function makeBulkCreateTicketsHandler(client) {
|
|
|
137
137
|
}
|
|
138
138
|
export const bulkAddTicketDependenciesToolConfig = {
|
|
139
139
|
title: 'Add many ticket dependencies in one call',
|
|
140
|
-
description: 'Create up to 200 blocked-by edges in one call
|
|
140
|
+
description: 'Create up to 200 blocked-by edges in one call. Use it instead of repeated orboto_add_ticket_dependency whenever wiring more than ~3 edges (dependency graphs after a bulk create). Each pair is (ticketKey, dependsOnKey) = "ticket is blocked by dependsOn". Per-pair error isolation; an already-existing edge counts as ok, a cycle rejection lands in `failed` with the API\'s reason.',
|
|
141
141
|
inputSchema: z.object({
|
|
142
142
|
pairs: z.array(z.object({
|
|
143
143
|
ticketKey: z.string().min(3).describe('The blocked ticket.'),
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
@@ -6,7 +6,7 @@ import { trimSimilarEntries } from './similar-projection.js';
|
|
|
6
6
|
import { resolveProjectByKey, resolveTicketByKey } from './shared.js';
|
|
7
7
|
export const checkSimilarToolConfig = {
|
|
8
8
|
title: 'Check for similar tickets before creating',
|
|
9
|
-
description: 'Run the duplicate
|
|
9
|
+
description: 'Run the duplicate check against a proposed title + description without creating anything. The pipeline is tsvector plus an AI-embedding rerank when configured. Returns up to `limit` candidates ranked by similarity. Use this BEFORE `orboto_create_ticket` when you want to confirm a feature is not already tracked - if a high-similarity candidate exists, prefer to comment on / claim / extend it instead of opening a new ticket. Empty result = safe to create. `orboto_create_ticket` runs the same check after the fact and surfaces `similarWarnings` in its response, so this tool is optional but cheaper than a create-then-close round trip. Only an AI-embedding candidate carries an absolute similarity; a full-text candidate prints as "text match" without a percentage (its rank is relative, so the best text match always scores 1.000) and can never trigger the hard duplicate-block, and neither can a match against a CLOSED ticket (ORB-2185). An empty result from a LONG, solution-framed title is weak evidence - detection ranks by term co-occurrence, so also probe with a single distinctive STABLE token (file/component/error-string fragment) and the SYMPTOM wording, not just your intended fix.',
|
|
10
10
|
inputSchema: z.object({
|
|
11
11
|
projectKey: z.string().min(1).describe('Project key (e.g. "ACME").'),
|
|
12
12
|
title: z.string().min(1).describe('Proposed ticket title.'),
|
|
@@ -11,7 +11,7 @@ import { z } from 'zod';
|
|
|
11
11
|
import { resolveTicketByKey } from './shared.js';
|
|
12
12
|
export const draftCustomerReplyToolConfig = {
|
|
13
13
|
title: 'Draft a customer-facing reply for a ticket',
|
|
14
|
-
description: 'Draft a reply
|
|
14
|
+
description: 'Draft a customer-facing reply about a ticket (by key). It is built server-side from customer-safe facts only (key, title, status, due date, customer summary, non-internal comments, your `ask`) - never from internal notes. Returns the draft, the facts used and what was excluded. Never posts: show it to the user, then `orboto_comment` only after approval. Use it for every customer-facing text.',
|
|
15
15
|
inputSchema: z.object({
|
|
16
16
|
ticketKey: z.string().describe('Ticket key like "ORB-42".'),
|
|
17
17
|
ask: z.string().min(3).max(1000).describe('What the reply should do, in the user\'s words, e.g. "tell them the fix ships with the next release" or "ask for the browser version".'),
|
|
@@ -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
|
|
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".'),
|
package/dist/tools/doc-edits.js
CHANGED
|
@@ -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
|
|
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({
|
package/dist/tools/doc-export.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/tools/docs.js
CHANGED
|
@@ -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`
|
|
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]]
|
|
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']),
|
|
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { resolveProjectByKey } from './shared.js';
|
|
8
8
|
export const getProjectPrimerToolConfig = {
|
|
9
9
|
title: 'Get project primer (AI Context Pack)',
|
|
10
|
-
description: 'Returns the project\'s auto-generated session primer in
|
|
10
|
+
description: 'Returns the project\'s auto-generated session primer in one call. It carries the project vocabulary a create has to get right (ticket type enum, the project\'s labels, its status names per category, its open milestone keys), active milestones, ticket counts, recently-closed milestones, top docs, structured primer facts, the Knowledge block (the index of the project wiki and of the Operations wiki, their latest log entries and open lint issues), and recent activity. Primer facts merge three scopes in order Workspace -> Project -> Personal: the Personal block ("Your preferences") is the calling user\'s own personal-scope facts, included only when that user has opted in (ai_preferences_enabled) and never showing another user\'s. Repo briefings (CLAUDE.md / AGENTS.md) are included ONLY when the operator configured them AND the API host can read those files from disk - most deployments will not have them, so do not assume CLAUDE.md / AGENTS.md content is in the response. Token-budget aware - pass `maxTokens` to constrain the output. Input is the project key (e.g. "ORB"), case-insensitive.',
|
|
11
11
|
inputSchema: z.object({
|
|
12
12
|
projectKey: z.string().min(1).describe('Project key, case-insensitive.'),
|
|
13
13
|
maxTokens: z.number().int().positive().max(200000).optional()
|
package/dist/tools/help.js
CHANGED
|
@@ -15,7 +15,7 @@ import { onboardingText } from '../agent-head.js';
|
|
|
15
15
|
const TOPICS = { connect: connectScriptText, onboarding: onboardingText };
|
|
16
16
|
export const helpToolConfig = {
|
|
17
17
|
title: 'Full guidance for one orboto tool',
|
|
18
|
-
description: 'Return the complete guidance text
|
|
18
|
+
description: 'Return the complete guidance text for one orboto tool by name. Manifest descriptions are one-line summaries; this adds the workflows, warnings and edge cases. Call it before first use of an unfamiliar write tool. topic: "connect" returns the step-by-step script for setting a person up, topic: "onboarding" the first five minutes and the work loop.',
|
|
19
19
|
inputSchema: z.object({
|
|
20
20
|
tool: z.string().min(1).max(128).optional().describe('Tool name, e.g. orboto_create_ticket.'),
|
|
21
21
|
topic: z.enum(['connect', 'onboarding']).optional().describe('Guidance topic instead of a tool.'),
|
package/dist/tools/identity.js
CHANGED
|
@@ -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}`
|
|
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(),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** ORB-2225 - the wiki as the agents' knowledge base: ask it first, file durable facts in it. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { mcpInstanceToken } from './shared.js';
|
|
4
|
+
export const knowledgeAskToolConfig = {
|
|
5
|
+
title: 'Ask the wiki',
|
|
6
|
+
description: 'Ask your session\'s project wikis and the Operations wiki before asking a person; cite docs by key.',
|
|
7
|
+
inputSchema: z.object({ question: z.string().min(3).max(1000), projectKeys: z.array(z.string().min(1).max(32)).max(20).optional() }).shape,
|
|
8
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
9
|
+
};
|
|
10
|
+
export function makeKnowledgeAskHandler(client) {
|
|
11
|
+
return async (input, extra) => {
|
|
12
|
+
const res = await client.post('/knowledge/ask', input, { instanceToken: mcpInstanceToken(undefined, extra) });
|
|
13
|
+
const where = res.spaces.map((s) => s.spaceKey ?? s.spaceName).join(', ');
|
|
14
|
+
const cites = res.citations.map((c) => `[${c.index}] ${c.title} - ${c.link}`).join('\n');
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: [res.answer, cites ? `Sources:\n${cites}` : '', `Asked: ${where}.`].filter(Boolean).join('\n\n') }],
|
|
17
|
+
structuredContent: res,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export const knowledgeAddToolConfig = {
|
|
22
|
+
title: 'File a fact in the wiki',
|
|
23
|
+
description: 'File a durable fact in a project wiki (projectKey) or the Operations wiki, never only in local memory.',
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
title: z.string().min(1).max(200), content: z.string().min(1).max(50_000),
|
|
26
|
+
projectKey: z.string().min(1).max(32).optional(), operations: z.boolean().optional(),
|
|
27
|
+
}).shape,
|
|
28
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
29
|
+
};
|
|
30
|
+
export function makeKnowledgeAddHandler(client) {
|
|
31
|
+
return async (input, extra) => {
|
|
32
|
+
const res = await client.post('/knowledge/add', input, { instanceToken: mcpInstanceToken(undefined, extra) });
|
|
33
|
+
return { content: [{ type: 'text', text: res.text }], structuredContent: res };
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { OrbotoClient as RealClient } from '../orboto-client.js';
|
|
3
|
+
import { knowledgeAddToolConfig, knowledgeAskToolConfig, makeKnowledgeAddHandler, makeKnowledgeAskHandler } from './knowledge.js';
|
|
4
|
+
import { KNOWLEDGE_BLOCK_CHARS, knowledgeLines, makeSessionStartHandler } from './session-start.js';
|
|
5
|
+
import { makeResponseExpandHandler } from './response-expand.js';
|
|
6
|
+
import { makeWikiRunsHandler } from './wiki.js';
|
|
7
|
+
function textOf(result) {
|
|
8
|
+
return result.content[0].text;
|
|
9
|
+
}
|
|
10
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
11
|
+
describe('ORB-2225 orboto_knowledge_ask and orboto_knowledge_add', () => {
|
|
12
|
+
it('asks the scope and lists the cited docs and the asked spaces', async () => {
|
|
13
|
+
const post = vi.fn().mockResolvedValue({
|
|
14
|
+
answer: 'Nightly with restic [1].', citations: [{ index: 1, title: 'Backup chain', link: '/spaces/s/docs/d' }], mode: 'keyword', abstained: false,
|
|
15
|
+
source: 'session', spaces: [{ spaceKey: 'ACME-S1', spaceName: 'ACME', projectKey: 'ACME' }, { spaceKey: 'SPACE-3', spaceName: 'Operations', projectKey: null }],
|
|
16
|
+
});
|
|
17
|
+
const result = await makeKnowledgeAskHandler({ post })({ question: 'where does the backup run?' }, { sessionId: 'abc' });
|
|
18
|
+
expect(post).toHaveBeenCalledWith('/knowledge/ask', { question: 'where does the backup run?' }, { instanceToken: 'mcp-abc' });
|
|
19
|
+
expect(textOf(result)).toContain('[1] Backup chain - /spaces/s/docs/d');
|
|
20
|
+
expect(textOf(result)).toContain('Asked: ACME-S1, SPACE-3.');
|
|
21
|
+
});
|
|
22
|
+
it('files a fact and returns the server sentence naming where it landed', async () => {
|
|
23
|
+
const post = vi.fn().mockResolvedValue({ action: 'recorded', text: 'Recorded in the ACME-S1 wiki: ACME-D7 "Build host". Cite it by key.', pages: [], source: null });
|
|
24
|
+
const result = await makeKnowledgeAddHandler({ post })({ title: 'Build host', content: 'build.example.org', projectKey: 'ACME' });
|
|
25
|
+
expect(post.mock.calls[0][0]).toBe('/knowledge/add');
|
|
26
|
+
expect(textOf(result)).toContain('ACME-D7');
|
|
27
|
+
expect(result.structuredContent).toMatchObject({ action: 'recorded' });
|
|
28
|
+
});
|
|
29
|
+
it('both tools carry a title and annotations; ask is read-only, add is not', () => {
|
|
30
|
+
expect(knowledgeAskToolConfig.title).toBeTruthy();
|
|
31
|
+
expect(knowledgeAskToolConfig.annotations.readOnlyHint).toBe(true);
|
|
32
|
+
expect(knowledgeAddToolConfig.title).toBeTruthy();
|
|
33
|
+
expect(knowledgeAddToolConfig.annotations.readOnlyHint).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('ORB-2225 the Knowledge block of orboto_session_start', () => {
|
|
37
|
+
it('prints the block from GET /knowledge with the session header', async () => {
|
|
38
|
+
const seen = [];
|
|
39
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
40
|
+
const u = new URL(url.toString());
|
|
41
|
+
const headers = new Headers(init?.headers);
|
|
42
|
+
seen.push({ path: u.pathname, session: headers.get('x-orboto-agent-session') });
|
|
43
|
+
const body = u.pathname === '/agent-instructions' ? { instructions: 'claim -> commit -> close', rulesHash: 'fixture' } : u.pathname === '/knowledge'
|
|
44
|
+
? { source: 'session', spaces: [{ spaceKey: 'ACME-S1', spaceName: 'ACME', projectKey: 'ACME', pages: 3, sources: 1, openLintIssues: 0, indexDoc: { key: 'ACME-D1' } }], missing: [], text: '## Knowledge\nIndex ACME-D1:\n- Build host' }
|
|
45
|
+
: {};
|
|
46
|
+
return { ok: true, status: 200, statusText: 'OK', json: async () => body, text: async () => '' };
|
|
47
|
+
});
|
|
48
|
+
const client = new RealClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
49
|
+
const result = await makeSessionStartHandler(client)({}, { sessionId: 'k1' });
|
|
50
|
+
expect(seen.find((c) => c.path === '/knowledge')?.session).toBe('mcp-k1');
|
|
51
|
+
expect(textOf(result)).toContain('## Knowledge\nIndex ACME-D1:\n- Build host');
|
|
52
|
+
expect(result.structuredContent).toMatchObject({ knowledge: { source: 'session', spaces: [{ spaceKey: 'ACME-S1', indexDocKey: 'ACME-D1', pages: 3 }] } });
|
|
53
|
+
});
|
|
54
|
+
it('cuts a long block at a line and serves the rest through a handle', async () => {
|
|
55
|
+
const text = ['## Knowledge', ...Array.from({ length: 200 }, (_, i) => `- ACME-D${i} Page ${i}`)].join('\n');
|
|
56
|
+
const block = knowledgeLines(text);
|
|
57
|
+
expect(block.lines.join('\n').length).toBeLessThan(KNOWLEDGE_BLOCK_CHARS + 200);
|
|
58
|
+
expect(block.lines.at(-1)).toContain(`handle: "${block.handle}"`);
|
|
59
|
+
const expanded = await makeResponseExpandHandler()({ handle: block.handle, path: 'knowledge' });
|
|
60
|
+
expect(textOf(expanded)).toContain('- ACME-D199 Page 199');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('ORB-2225 orboto_wiki_runs', () => {
|
|
64
|
+
it('lists the failed runs with their error and retries one by id', async () => {
|
|
65
|
+
const get = vi.fn().mockResolvedValue({ runs: [{ id: 'r1', sourceKey: 'ACME-D9', sourceTitle: 'Runner notes', status: 'failed', error: 'answer budget spent', createdAt: '2026-09-27T08:00:00Z' }] });
|
|
66
|
+
const post = vi.fn().mockResolvedValue({ runId: 'r1', status: 'applied', queued: false, error: null });
|
|
67
|
+
const handler = makeWikiRunsHandler({ get, post });
|
|
68
|
+
expect(textOf(await handler({ spaceId: 'ACME-S1', status: 'failed' }))).toContain('r1 failed ACME-D9 "Runner notes" 2026-09-27T08:00:00Z - answer budget spent');
|
|
69
|
+
expect(get).toHaveBeenCalledWith('/spaces/ACME-S1/llm-wiki/ingest-runs?status=failed');
|
|
70
|
+
expect((await handler({ spaceId: 'ACME-S1', action: 'retry' })).isError).toBe(true);
|
|
71
|
+
expect(textOf(await handler({ spaceId: 'ACME-S1', action: 'retry', runId: '11111111-0000-4000-8000-000000000000' }))).toContain('ran again: applied');
|
|
72
|
+
expect(post).toHaveBeenCalledWith('/spaces/ACME-S1/llm-wiki/ingest-runs/11111111-0000-4000-8000-000000000000/retry', {});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -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
|
|
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
|
|
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.'),
|
package/dist/tools/raci.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|
package/dist/tools/sentry.js
CHANGED
|
@@ -9,7 +9,7 @@ import { mcpInstanceToken, resolveProjectByKey, resolveTicketByKey } from './sha
|
|
|
9
9
|
const ACTIONS = ['list', 'health', 'connect', 'update', 'rotate_secret', 'disconnect', 'triage_lane', 'verdict'];
|
|
10
10
|
export const sentryToolConfig = {
|
|
11
11
|
title: 'Sentry connector',
|
|
12
|
-
description: 'Sentry connector (ORB-2123): list, health, connect (orgSlug, projectSlug, clientSecret), update (triageMode comment|fix_branch), rotate_secret, disconnect, triage_lane (laneId, empty unbinds), verdict (ticketKey, verdict known_fixed|regression|new, summary). Secrets never come back; docs/sentry-intake.md.',
|
|
12
|
+
description: 'Sentry connector (ORB-2123). Actions: list, health, connect (orgSlug, projectSlug, clientSecret), update (triageMode comment|fix_branch), rotate_secret, disconnect, triage_lane (laneId, empty unbinds), verdict (ticketKey, verdict known_fixed|regression|new, summary). Secrets never come back; docs/sentry-intake.md.',
|
|
13
13
|
inputSchema: z.object({
|
|
14
14
|
action: z.enum(ACTIONS),
|
|
15
15
|
projectKey: z.string().min(1),
|
|
@@ -21,7 +21,8 @@ import { agentHeadText } from '../agent-head.js';
|
|
|
21
21
|
export const sessionStartToolConfig = {
|
|
22
22
|
title: 'Load the rules you must follow + re-orient',
|
|
23
23
|
description: 'THE canonical way to LOAD the binding workspace rules you must follow as an agent. Run it as your FIRST action in a session and immediately AFTER any context compaction. Returns a one-line-per-rule INDEX of the binding working-rules plus their hash (or a compact "unchanged" ack on a repeat call within the same connection) - read the index, then call this tool again with `rulesOnly: true` to read the full text of the rules whenever you do not already hold that exact hash, and expand before acting on any rule whose title touches what you are about to do. Also returns your in-progress tickets - each flagged LANDED, IDLE when it has a linked commit but has not moved for days, i.e. finished work you never handed to review - your running timer, and a warning if a project\'s git connection looks unhealthy (commit ingestion may be stalled). Pass `ticketKey` to also get a one-shot bundle for that ticket: project primer, the full ticket with dependencies + checklists, that project\'s git health, and any other agent sessions currently on it - replacing several separate calls. (Do NOT use orboto_list_agent_instructions to read the rules - that tool MANAGES/edits rule blocks for admins; this one is what you read to know how to work.) Registers this session (a heartbeat on its instance row) and is otherwise read-only. '
|
|
24
|
-
+ '**Parameters.** `rulesOnly: true` returns ONLY the complete rules text (nothing else) and is never truncated - the cheapest way to read the rules the index listed. `projectId` adds that project\'s rules on top of the workspace + personal ones. `ticketKey` ("ACME-42") bundles that ticket\'s primer, full detail, dependencies, checklists, git health and other active agent sessions into the same response. `forceRules: true` returns the full rules text inline with the rest of the digest even when this connection already delivered them - use it whenever the rules are NOT in your context right now: after a compaction, a /clear, or a fresh agent taking over an existing connection. `agentKind` (coding, orchestrator, reviewer, runner) and `modelTier` (frontier, standard, small) are your self-declared classification; rule blocks and per-tier rule text are targeted by them. `scope` { projectKeys, ticketKeys, role } declares this session\'s responsibility; the returned session.ref is what peers pass as toSessionRef (ORB-2136).'
|
|
24
|
+
+ '**Parameters.** `rulesOnly: true` returns ONLY the complete rules text (nothing else) and is never truncated - the cheapest way to read the rules the index listed. `projectId` adds that project\'s rules on top of the workspace + personal ones. `ticketKey` ("ACME-42") bundles that ticket\'s primer, full detail, dependencies, checklists, git health and other active agent sessions into the same response. `forceRules: true` returns the full rules text inline with the rest of the digest even when this connection already delivered them - use it whenever the rules are NOT in your context right now: after a compaction, a /clear, or a fresh agent taking over an existing connection. `agentKind` (coding, orchestrator, reviewer, runner) and `modelTier` (frontier, standard, small) are your self-declared classification; rule blocks and per-tier rule text are targeted by them. `scope` { projectKeys, ticketKeys, role } declares this session\'s responsibility; the returned session.ref is what peers pass as toSessionRef (ORB-2136). '
|
|
25
|
+
+ 'The digest also carries the Knowledge block (ORB-2225): the wiki index of your declared projects and of the Operations wiki, their latest log entries and open lint issues; ask the wiki with orboto_knowledge_ask before asking a person, file durable facts with orboto_knowledge_add.',
|
|
25
26
|
inputSchema: z.object({
|
|
26
27
|
projectId: z.string().uuid().optional().describe('Also load this project\'s rules.'),
|
|
27
28
|
ticketKey: z.string().min(3).optional().describe('Ticket key ("ACME-42") - bundles that ticket\'s full context.'),
|
|
@@ -132,6 +133,30 @@ async function buildTicketBundle(client, ticketKey) {
|
|
|
132
133
|
},
|
|
133
134
|
};
|
|
134
135
|
}
|
|
136
|
+
/** ORB-2225 - the Knowledge block stays small; the rest is one expand away. */
|
|
137
|
+
export const KNOWLEDGE_BLOCK_CHARS = 1500;
|
|
138
|
+
export function knowledgeLines(text) {
|
|
139
|
+
if (text.length <= KNOWLEDGE_BLOCK_CHARS)
|
|
140
|
+
return { lines: text.split('\n') };
|
|
141
|
+
const cut = text.slice(0, KNOWLEDGE_BLOCK_CHARS);
|
|
142
|
+
const kept = cut.slice(0, Math.max(cut.lastIndexOf('\n'), 0));
|
|
143
|
+
const handle = storePayload('orboto_session_start', {
|
|
144
|
+
structuredContent: { knowledge: text }, text,
|
|
145
|
+
omitted: [{ path: 'knowledge', kind: 'string', omittedChars: text.length - kept.length }],
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
handle,
|
|
149
|
+
lines: [...kept.split('\n'), `(Knowledge block cut at ${kept.length} of ${text.length} characters: orboto_response_expand { handle: "${handle}", path: "knowledge" }, or GET /knowledge.)`],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function knowledgeSummary(index, handle) {
|
|
153
|
+
return {
|
|
154
|
+
source: index.source,
|
|
155
|
+
spaces: index.spaces.map((s) => ({ spaceKey: s.spaceKey ?? s.spaceName, projectKey: s.projectKey, indexDocKey: s.indexDoc?.key ?? null, pages: s.pages, sources: s.sources, openLintIssues: s.openLintIssues })),
|
|
156
|
+
missing: index.missing,
|
|
157
|
+
...(handle ? { handle } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
135
160
|
/** ORB-2209 - a declared scope stays with the instance token; the agent declares it once per terminal. */
|
|
136
161
|
export function identityLine(registration) {
|
|
137
162
|
const count = registration.reconnects ?? 0;
|
|
@@ -185,7 +210,7 @@ export function makeSessionStartHandler(client, opts = {}) {
|
|
|
185
210
|
if (input.scope === undefined && registration && !registration.scope && envProjectScope()) {
|
|
186
211
|
registration = toRegistration(await declareEnvProjectScope(client, instanceToken)) ?? registration;
|
|
187
212
|
}
|
|
188
|
-
const [me, rules, assigned, timer, inboxRaw] = await Promise.all([
|
|
213
|
+
const [me, rules, assigned, timer, inboxRaw, knowledge] = await Promise.all([
|
|
189
214
|
client.get('/users/me').catch(() => null),
|
|
190
215
|
loadRequiredRules(client, rulesPath, rulesParams.get('knownRulesHash') ?? undefined),
|
|
191
216
|
client.get('/users/me/assigned-tickets?statuses=IN_PROGRESS,IN_REVIEW&limit=20').catch(() => ({ items: [] })),
|
|
@@ -193,6 +218,9 @@ export function makeSessionStartHandler(client, opts = {}) {
|
|
|
193
218
|
client.get('/v1/agent/messages?limit=10', { instanceToken })
|
|
194
219
|
.then((r) => (Array.isArray(r?.messages) ? r.messages : []))
|
|
195
220
|
.catch(() => []),
|
|
221
|
+
client.get('/knowledge', { instanceToken })
|
|
222
|
+
.then((r) => (typeof r?.text === 'string' && Array.isArray(r.spaces) && Array.isArray(r.missing) ? r : null))
|
|
223
|
+
.catch(() => null),
|
|
196
224
|
]);
|
|
197
225
|
const pendingMessages = inboxRaw.map((m) => ({ id: m.id, fromUserId: m.fromUserId, from: m.from?.label ?? m.fromUserId, kind: m.kind, subject: m.subject, createdAt: m.createdAt }));
|
|
198
226
|
const tickets = Array.isArray(assigned) ? assigned : (assigned?.items ?? []);
|
|
@@ -267,6 +295,9 @@ export function makeSessionStartHandler(client, opts = {}) {
|
|
|
267
295
|
}
|
|
268
296
|
lines.push('', '## Timer');
|
|
269
297
|
lines.push(timer?.ticketId ? `Running on ${timer.ticketKey ?? timer.ticketId} since ${timer.startedAt ?? 'earlier'}.` : 'No timer running.');
|
|
298
|
+
const knowledgeBlock = knowledge?.text ? knowledgeLines(knowledge.text) : null;
|
|
299
|
+
if (knowledgeBlock)
|
|
300
|
+
lines.push('', ...knowledgeBlock.lines);
|
|
270
301
|
lines.push('', '## This session');
|
|
271
302
|
const scopeText = registration?.scope
|
|
272
303
|
? [registration.scope.role ? `role ${registration.scope.role}` : null, registration.scope.projectKeys?.length ? `projects ${registration.scope.projectKeys.join(', ')}` : null, registration.scope.ticketKeys?.length ? `tickets ${registration.scope.ticketKeys.join(', ')}` : null].filter(Boolean).join('; ')
|
|
@@ -315,6 +346,7 @@ export function makeSessionStartHandler(client, opts = {}) {
|
|
|
315
346
|
.filter((p) => p.connections.length > 0),
|
|
316
347
|
healthyCount: gitHealthWithConnections.reduce((n, p) => n + p.connections.filter((c) => c.healthy).length, 0),
|
|
317
348
|
},
|
|
349
|
+
...(knowledge ? { knowledge: knowledgeSummary(knowledge, knowledgeBlock?.handle) } : {}),
|
|
318
350
|
...(pendingMessages.length > 0 ? { pendingMessages } : {}),
|
|
319
351
|
...(bundle ? { ticketBundle: bundle.structured } : {}),
|
|
320
352
|
},
|
|
@@ -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
|
|
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
|
|
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):
|
|
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
|
|
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
|
|
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(),
|
package/dist/tools/wiki.js
CHANGED
|
@@ -54,7 +54,7 @@ export function makeWikiAskHandler(client) {
|
|
|
54
54
|
}
|
|
55
55
|
export const wikiLintToolConfig = {
|
|
56
56
|
title: 'Run the LLM-Wiki lint pass on a space',
|
|
57
|
-
description: '
|
|
57
|
+
description: 'Lint an LLM-Wiki space for inconsistencies. It finds orphan pages, missing cross-references, stale pages, unprocessed sources and, when AI is configured, contradictions and undocumented concepts. Returns the open issues, each with a suggested fix. Wraps POST /spaces/:id/llm-wiki/lint.',
|
|
58
58
|
inputSchema: z.object({
|
|
59
59
|
spaceId: z.string().uuid().describe('The LLM-Wiki space to lint.'),
|
|
60
60
|
}).shape,
|
|
@@ -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
|
|
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.'),
|
|
@@ -91,7 +91,7 @@ export function makeWikiPlanUpdateHandler(client) {
|
|
|
91
91
|
}
|
|
92
92
|
export const wikiApplyPlanToolConfig = {
|
|
93
93
|
title: 'Apply a previously-planned wiki edit',
|
|
94
|
-
description: 'Commit the page operations from a plan created by orboto_wiki_plan_update. Applies every op atomically (each page edit is snapshotted for rollback). Fails with 410 if the plan has expired (>15 min) or was already applied. Wraps POST /spaces/:id/docs/apply-plan.',
|
|
94
|
+
description: 'Commit the page operations from a plan created by orboto_wiki_plan_update. Applies every op atomically (each page edit is snapshotted for rollback), then rebuilds the index doc of the space and appends a line to its log doc. Fails with 410 if the plan has expired (>15 min) or was already applied. Wraps POST /spaces/:id/docs/apply-plan.',
|
|
95
95
|
inputSchema: z.object({
|
|
96
96
|
spaceId: z.string().uuid(),
|
|
97
97
|
planId: z.string().uuid().describe('The planId returned by orboto_wiki_plan_update.'),
|
|
@@ -106,7 +106,7 @@ export function makeWikiApplyPlanHandler(client) {
|
|
|
106
106
|
}
|
|
107
107
|
export const wikiRecordToolConfig = {
|
|
108
108
|
title: 'Record a wiki update in one step (plan + apply)',
|
|
109
|
-
description: '
|
|
109
|
+
description: 'Plan and apply a wiki edit in one step. Use it mid-task to capture a fact or update a page without the two-step review loop. Internally calls plan-update then apply-plan. For a reviewable change, use orboto_wiki_plan_update instead.',
|
|
110
110
|
inputSchema: z.object({
|
|
111
111
|
spaceId: z.string().uuid(),
|
|
112
112
|
instruction: z.string().min(1).max(4000).describe('What to record, in plain language.'),
|
|
@@ -175,3 +175,28 @@ export function makeWikiFlagStaleHandler(client) {
|
|
|
175
175
|
return text(res.staleFlagged ? 'Page flagged as possibly outdated.' : 'Stale flag cleared.', { staleFlagged: res.staleFlagged });
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
+
export const wikiRunsToolConfig = {
|
|
179
|
+
title: 'Wiki ingest runs',
|
|
180
|
+
description: 'List the ingest runs of a wiki space (status failed shows each error) or retry a failed run.',
|
|
181
|
+
inputSchema: z.object({
|
|
182
|
+
spaceId: z.string().min(1),
|
|
183
|
+
action: z.enum(['list', 'retry']).optional(),
|
|
184
|
+
runId: z.string().uuid().optional(),
|
|
185
|
+
status: z.enum(['pending_review', 'applied', 'failed', 'skipped']).optional(),
|
|
186
|
+
}).shape,
|
|
187
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
188
|
+
};
|
|
189
|
+
export function makeWikiRunsHandler(client) {
|
|
190
|
+
return async (input) => {
|
|
191
|
+
if (input.action === 'retry') {
|
|
192
|
+
if (!input.runId)
|
|
193
|
+
return { isError: true, content: [{ type: 'text', text: 'action retry needs the runId of a failed run (action list, status failed).' }] };
|
|
194
|
+
const res = await client.post(`/spaces/${encodeURIComponent(input.spaceId)}/llm-wiki/ingest-runs/${input.runId}/retry`, {});
|
|
195
|
+
return text(res.queued ? `Run ${res.runId} is queued for the ingest worker again.` : `Run ${res.runId} ran again: ${res.status}${res.error ? ` - ${res.error}` : ''}.`, res);
|
|
196
|
+
}
|
|
197
|
+
const qs = input.status ? `?status=${input.status}` : '';
|
|
198
|
+
const res = await client.get(`/spaces/${encodeURIComponent(input.spaceId)}/llm-wiki/ingest-runs${qs}`);
|
|
199
|
+
const lines = res.runs.map((r) => `- ${r.id} ${r.status} ${r.sourceKey ?? ''} "${r.sourceTitle}" ${r.createdAt}${r.error ? ` - ${r.error}` : ''}`);
|
|
200
|
+
return text(lines.length ? lines.join('\n') : 'No ingest runs.', res);
|
|
201
|
+
};
|
|
202
|
+
}
|
package/dist/tools/work-next.js
CHANGED
|
@@ -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
|
|
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()
|
|
@@ -314,7 +314,7 @@ export function makeWorkSessionStartHandler(client) {
|
|
|
314
314
|
}
|
|
315
315
|
export const workSessionFinishToolConfig = {
|
|
316
316
|
title: 'Finish a work session (books time, frees the lease)',
|
|
317
|
-
description: 'End a work session:
|
|
317
|
+
description: 'End a work session: timer booked, lease released, evidence recorded. The timer is stopped and booked, the (ticket, role) lease is released for the next agent, and the evidence you pass (commit sha + which gates you ran) is recorded on the session. Idempotent - finishing an already-finished session succeeds and still absorbs late evidence, so a retrying harness never has to distinguish "already done" from "failed". This does NOT close the ticket; ticket status is a separate, deliberate decision (use orboto_close_ticket once the acceptance criteria are verified). Prefer orboto_work_finish (ORB-1612) for an `implementation` session that is actually done - it does this AND the ticket transition AND the completion note in one call.',
|
|
318
318
|
inputSchema: z.object({
|
|
319
319
|
sessionId: z.string().uuid().describe('The id returned by orboto_work_session_start.'),
|
|
320
320
|
outcome: z.enum(['finished', 'cancelled']).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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.209.0",
|
|
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",
|