@orboto/mcp 0.192.0 → 0.194.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/event-bridge.js +1 -47
- package/dist/event-bridge.test.js +0 -19
- package/dist/http-transport-binding.test.js +1 -4
- package/dist/http-transport.js +1 -213
- package/dist/http-transport.test.js +0 -56
- package/dist/index.js +1 -48
- package/dist/input-schema.js +2 -76
- package/dist/input-schema.test.js +1 -15
- package/dist/instructions-budget.test.js +0 -2
- package/dist/manifest-size.test.js +3 -72
- package/dist/oauth-bootstrap.js +0 -47
- package/dist/oauth-bootstrap.test.js +4 -16
- package/dist/orboto-client.js +0 -30
- package/dist/orboto-client.test.js +0 -12
- package/dist/prompts.js +0 -39
- package/dist/protect-text-usage.test.js +1 -9
- package/dist/required-rules-wrapper.test.js +0 -5
- package/dist/required-rules.js +0 -2
- package/dist/resources.js +0 -62
- package/dist/resources.test.js +0 -16
- package/dist/response-budget.js +2 -150
- package/dist/response-budget.test.js +1 -32
- package/dist/server.js +4 -180
- package/dist/session-nudge.js +0 -9
- package/dist/session-nudge.test.js +2 -10
- package/dist/session-nudge.transport.test.js +0 -23
- package/dist/tool-annotations.test.js +0 -44
- package/dist/tool-docs.js +1 -12
- package/dist/tool-docs.test.js +2 -8
- package/dist/tool-parameters.test.js +0 -19
- package/dist/tools/absence-writes.js +1 -17
- package/dist/tools/admin-translations.js +0 -11
- package/dist/tools/admin-writes.js +0 -28
- package/dist/tools/admin-writes.test.js +0 -1
- package/dist/tools/agent-coordination.js +2 -24
- package/dist/tools/agent-coordination.test.js +0 -2
- package/dist/tools/agent-instructions.js +0 -10
- package/dist/tools/agent-instructions.test.js +0 -2
- package/dist/tools/agent-messages.js +0 -1
- package/dist/tools/ai-status.js +0 -14
- package/dist/tools/ai-usage.js +0 -10
- package/dist/tools/analytics.js +0 -2
- package/dist/tools/api-call.js +0 -12
- package/dist/tools/api-call.test.js +1 -2
- package/dist/tools/api-search.js +1 -6
- package/dist/tools/approvals.js +0 -6
- package/dist/tools/attach.js +0 -25
- package/dist/tools/backup.js +0 -13
- package/dist/tools/backup.test.js +1 -2
- package/dist/tools/bulk-create.js +1 -25
- package/dist/tools/bulk-create.test.js +0 -4
- package/dist/tools/bulk-writes.js +69 -61
- package/dist/tools/bulk-writes.test.js +34 -4
- package/dist/tools/check-similar.js +0 -17
- package/dist/tools/checklist-writes.js +82 -27
- package/dist/tools/checklist-writes.test.js +69 -1
- package/dist/tools/claim.js +1 -71
- package/dist/tools/claim.test.js +0 -2
- package/dist/tools/critical-path.js +0 -5
- package/dist/tools/critical-path.test.js +0 -3
- package/dist/tools/cross-project-links.ee.js +1 -24
- package/dist/tools/doc-attachments.js +0 -28
- package/dist/tools/doc-attachments.test.js +1 -4
- package/dist/tools/doc-comments.js +1 -37
- package/dist/tools/doc-comments.test.js +1 -2
- package/dist/tools/doc-edits.js +1 -31
- package/dist/tools/doc-edits.test.js +0 -9
- package/dist/tools/doc-export.js +0 -24
- package/dist/tools/doc-export.test.js +0 -12
- package/dist/tools/doc-revisions.js +0 -23
- package/dist/tools/docs-ai.js +0 -34
- package/dist/tools/docs-ai.test.js +0 -11
- package/dist/tools/docs.js +1 -75
- package/dist/tools/docs.test.js +1 -3
- package/dist/tools/embedding-status.js +0 -13
- package/dist/tools/embedding-status.test.js +0 -1
- package/dist/tools/feedback.js +1 -15
- package/dist/tools/feedback.test.js +0 -1
- package/dist/tools/get-checklists.js +6 -14
- package/dist/tools/get-project-primer.js +1 -25
- package/dist/tools/get-project.js +0 -17
- package/dist/tools/get-ticket.js +1 -61
- package/dist/tools/get-timer.js +0 -11
- package/dist/tools/git-health-reasons.js +0 -1
- package/dist/tools/identity.js +0 -13
- package/dist/tools/list-projects.js +0 -20
- package/dist/tools/list-projects.test.js +0 -1
- package/dist/tools/list-tickets.js +10 -9
- package/dist/tools/milestones-crud.test.js +0 -4
- package/dist/tools/milestones.js +0 -43
- package/dist/tools/my-tickets.js +0 -7
- package/dist/tools/phase-b-tools.test.js +14 -21
- package/dist/tools/primer-facts.js +1 -49
- package/dist/tools/primer-facts.test.js +0 -20
- package/dist/tools/project-listings.js +0 -25
- package/dist/tools/query.js +1 -12
- package/dist/tools/query.test.js +0 -8
- package/dist/tools/raci.js +0 -11
- package/dist/tools/requirements-spec.js +1 -12
- package/dist/tools/response-expand.js +0 -19
- package/dist/tools/review-policy.js +0 -24
- package/dist/tools/review-policy.test.js +0 -2
- package/dist/tools/search.js +1 -13
- package/dist/tools/session-start.js +1 -136
- package/dist/tools/session-start.test.js +1 -44
- package/dist/tools/set-parent.js +1 -19
- package/dist/tools/shared.js +0 -41
- package/dist/tools/shared.test.js +0 -4
- package/dist/tools/similar-projection.js +0 -15
- package/dist/tools/ticket-attachments.js +1 -25
- package/dist/tools/ticket-schedules.js +0 -9
- package/dist/tools/ticket-schedules.test.js +0 -1
- package/dist/tools/ticket-writes.js +1 -110
- package/dist/tools/ticket-writes.test.js +1 -15
- package/dist/tools/time-writes.js +26 -46
- package/dist/tools/time-writes.test.js +18 -0
- package/dist/tools/update-project.js +1 -27
- package/dist/tools/update-project.test.js +1 -1
- package/dist/tools/wiki.js +0 -8
- package/dist/tools/work-next.js +0 -5
- package/dist/tools/work-sessions-start.test.js +0 -2
- package/dist/tools/work-sessions.js +0 -48
- package/dist/tools/work-sessions.test.js +0 -5
- package/dist/toolset.js +2 -81
- package/dist/toolset.test.js +0 -20
- package/dist/version-guard.test.js +0 -4
- package/dist/version.js +0 -12
- package/dist/with-metrics-output-schema.test.js +1 -14
- package/dist/with-metrics.js +2 -74
- package/dist/with-metrics.test.js +7 -29
- package/package.json +1 -1
|
@@ -2,32 +2,12 @@
|
|
|
2
2
|
* ORB-885 - `orboto_update_project`.
|
|
3
3
|
* ORB-830 - `orboto_create_project` + `orboto_archive_project`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (`orboto_list_projects`, `orboto_get_project`) shipped in ORB-244
|
|
7
|
-
* Phase B; ORB-885 added `update_project`, ORB-830 closes the gap
|
|
8
|
-
* with create + archive so an agent never has to drop to a raw
|
|
9
|
-
* `POST /projects` to spin up a new project.
|
|
10
|
-
*
|
|
11
|
-
* Status mutation is allowed in `update_project` (unlike
|
|
12
|
-
* `orboto_update_milestone`, which delegates closing to a dedicated
|
|
13
|
-
* tool) because project status is a four-state lifecycle (`draft` →
|
|
14
|
-
* `active` → `archived` / `closed`) with no extra semantics - a
|
|
15
|
-
* single patch field covers it cleanly. `archive_project` is a
|
|
16
|
-
* thin convenience over `update_project({ status: 'archived' })`
|
|
17
|
-
* because "archive this project" is a workflow agents reach for
|
|
18
|
-
* often enough that an explicit verb beats teaching the model the
|
|
19
|
-
* patch shape.
|
|
5
|
+
* @see ORB-244
|
|
20
6
|
*/
|
|
21
7
|
import { z } from 'zod';
|
|
22
8
|
import { resolveProjectByKey } from './shared.js';
|
|
23
|
-
// Mirrors the Zod schema on `PATCH /projects/:id` (see
|
|
24
|
-
// apps/api/src/routes/projects.ts). Kept in sync manually - there's no
|
|
25
|
-
// generator from the API's runtime schema into the MCP package yet.
|
|
26
9
|
const PROJECT_KEY_RE = /^[A-Z0-9]+$/;
|
|
27
10
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28
|
-
// ORB-993/ORB-994 - the 18 detector-supported workspace/project locales.
|
|
29
|
-
// Mirrors WORKSPACE_LOCALE_CODES in @orboto/shared-schema (kept in sync
|
|
30
|
-
// manually, same as the rest of this tool's schema).
|
|
31
11
|
const LOCALE_CODES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'ru', 'pl', 'tr', 'cs', 'da', 'sv', 'no', 'fi', 'ja', 'zh', 'ko'];
|
|
32
12
|
export const updateProjectToolConfig = {
|
|
33
13
|
title: 'Update a project\'s metadata',
|
|
@@ -70,9 +50,6 @@ export function makeUpdateProjectHandler(client) {
|
|
|
70
50
|
};
|
|
71
51
|
};
|
|
72
52
|
}
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// orboto_create_project - ORB-830
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
53
|
export const createProjectToolConfig = {
|
|
77
54
|
title: 'Create a new project',
|
|
78
55
|
description: 'Create a new project in the workspace. `name` is required; `key` is optional and the API auto-derives one from the name when omitted (uppercase initials). Returns the new project\'s id + key + status (`active` by default). Caller must have `admin:project:create` or be a super-admin. Note: a new project is auto-provisioned with a general doc space (slug `<key>-general`, system-generated, for the AI primer + notes) - don\'t create a separate doc space for its docs, reuse that one (orboto_list_doc_spaces).',
|
|
@@ -116,9 +93,6 @@ export function makeCreateProjectHandler(client) {
|
|
|
116
93
|
};
|
|
117
94
|
};
|
|
118
95
|
}
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// orboto_archive_project - ORB-830
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
96
|
export const archiveProjectToolConfig = {
|
|
123
97
|
title: 'Archive a project',
|
|
124
98
|
description: 'Archive a project - moves it out of the active rotation without deleting any data. Convenience wrapper around `orboto_update_project({ status: "archived" })` because "archive this project" is a workflow operators hit often. Archiving is reversible: pass the same project back through `orboto_update_project({ status: "active" })` to restore. Idempotent - re-archiving an already-archived project is a no-op success.',
|
|
@@ -152,7 +152,7 @@ describe('orboto_archive_project', () => {
|
|
|
152
152
|
{ json: { ...PROJ, status: 'archived' } },
|
|
153
153
|
]);
|
|
154
154
|
const res = await makeArchiveProjectHandler(client)({ projectKey: 'ACME' });
|
|
155
|
-
expect(calls).toHaveLength(1);
|
|
155
|
+
expect(calls).toHaveLength(1);
|
|
156
156
|
expect(res.content[0].text).toMatch(/already archived/);
|
|
157
157
|
expect(res.structuredContent).toMatchObject({ alreadyArchived: true });
|
|
158
158
|
});
|
package/dist/tools/wiki.js
CHANGED
|
@@ -11,7 +11,6 @@ import { resolveDocId } from './docs.js';
|
|
|
11
11
|
function text(t, structured) {
|
|
12
12
|
return { content: [{ type: 'text', text: t }], ...(structured ? { structuredContent: structured } : {}) };
|
|
13
13
|
}
|
|
14
|
-
// --- orboto_wiki_ingest_url ------------------------------------------------
|
|
15
14
|
export const wikiIngestUrlToolConfig = {
|
|
16
15
|
title: 'Ingest a URL into an LLM-Wiki space',
|
|
17
16
|
description: 'Fetch a public URL, extract its main article as Markdown, and create a SOURCE doc in the space. When the space has the LLM-Wiki enabled, this also enqueues the auto-ingest worker, which curates the source into wiki pages (immediately under auto-apply, or as a pending plan under review-gate). Wraps POST /spaces/:id/docs/ingest-url.',
|
|
@@ -31,7 +30,6 @@ export function makeWikiIngestUrlHandler(client) {
|
|
|
31
30
|
return text(`Ingested source "${res.title}" (docId: ${res.docId}). If the space is LLM-Wiki-enabled the curation worker is now processing it.`, { docId: res.docId, title: res.title });
|
|
32
31
|
};
|
|
33
32
|
}
|
|
34
|
-
// --- orboto_wiki_ask -------------------------------------------------------
|
|
35
33
|
export const wikiAskToolConfig = {
|
|
36
34
|
title: 'Ask a question over the wiki (RAG with citations)',
|
|
37
35
|
description: 'Retrieval-augmented Q&A over the docs corpus, optionally scoped to one space. Returns an answer with numbered citations linking back to source docs. Requires both a chat AND an embedding provider (check orboto_ai_status). Wraps POST /ai/ask-docs.',
|
|
@@ -54,7 +52,6 @@ export function makeWikiAskHandler(client) {
|
|
|
54
52
|
return text(`${res.answer}\n\n${cites ? `Sources:\n${cites}` : ''}`.trim(), { answer: res.answer, citations: res.citations, mode: res.mode });
|
|
55
53
|
};
|
|
56
54
|
}
|
|
57
|
-
// --- orboto_wiki_lint ------------------------------------------------------
|
|
58
55
|
export const wikiLintToolConfig = {
|
|
59
56
|
title: 'Run the LLM-Wiki lint pass on a space',
|
|
60
57
|
description: 'Scan an LLM-Wiki space for inconsistencies (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.',
|
|
@@ -72,7 +69,6 @@ export function makeWikiLintHandler(client) {
|
|
|
72
69
|
return text(`Lint complete: ${res.issues.length} open issue(s), ${res.resolved} auto-resolved.\n${lines.join('\n')}`, { issues: res.issues, detected: res.detected, resolved: res.resolved });
|
|
73
70
|
};
|
|
74
71
|
}
|
|
75
|
-
// --- orboto_wiki_plan_update / apply_plan ----------------------------------
|
|
76
72
|
export const wikiPlanUpdateToolConfig = {
|
|
77
73
|
title: 'Plan a wiki edit (dry-run, no writes)',
|
|
78
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.',
|
|
@@ -108,7 +104,6 @@ export function makeWikiApplyPlanHandler(client) {
|
|
|
108
104
|
return text(`Applied plan ${input.planId}: ${res.touchedDocs.length} page(s) updated.`, { touchedDocs: res.touchedDocs });
|
|
109
105
|
};
|
|
110
106
|
}
|
|
111
|
-
// --- orboto_wiki_record (convenience: plan + apply) ------------------------
|
|
112
107
|
export const wikiRecordToolConfig = {
|
|
113
108
|
title: 'Record a wiki update in one step (plan + apply)',
|
|
114
109
|
description: 'Convenience wrapper that plans an edit from your instruction and immediately applies it - use 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.',
|
|
@@ -129,7 +124,6 @@ export function makeWikiRecordHandler(client) {
|
|
|
129
124
|
return text(`Recorded: ${applied.touchedDocs.length} page(s) updated.`, { planId: plan.planId, touchedDocs: applied.touchedDocs });
|
|
130
125
|
};
|
|
131
126
|
}
|
|
132
|
-
// --- orboto_wiki_append_section -------------------------------------------
|
|
133
127
|
export const wikiAppendSectionToolConfig = {
|
|
134
128
|
title: 'Append a section to a wiki page (idempotent)',
|
|
135
129
|
description: 'Append a Markdown section to a doc. Idempotent: appending identical content a second time is a no-op, so this is safe to call repeatedly (e.g. across multiple ingests). Wraps POST /docs/:id/append-section.',
|
|
@@ -146,7 +140,6 @@ export function makeWikiAppendSectionHandler(client) {
|
|
|
146
140
|
return text(res.appended ? 'Section appended.' : 'No change - an identical section is already present.', { appended: res.appended });
|
|
147
141
|
};
|
|
148
142
|
}
|
|
149
|
-
// --- orboto_wiki_save_answer -----------------------------------------------
|
|
150
143
|
export const wikiSaveAnswerToolConfig = {
|
|
151
144
|
title: 'Save a Q&A answer as a wiki page',
|
|
152
145
|
description: 'Turn an answer (e.g. from orboto_wiki_ask) into a curated wiki page with smart-links to its citations. Idempotent per (space, question): re-saving the same question updates the page instead of duplicating it. Wraps POST /ai/save-answer-to-wiki.',
|
|
@@ -166,7 +159,6 @@ export function makeWikiSaveAnswerHandler(client) {
|
|
|
166
159
|
return text(res.created ? `Saved as a new wiki page (docId: ${res.docId}).` : `Updated the existing wiki page (docId: ${res.docId}).`, { docId: res.docId, created: res.created });
|
|
167
160
|
};
|
|
168
161
|
}
|
|
169
|
-
// --- orboto_wiki_flag_stale ------------------------------------------------
|
|
170
162
|
export const wikiFlagStaleToolConfig = {
|
|
171
163
|
title: 'Flag (or unflag) a wiki page as possibly outdated',
|
|
172
164
|
description: 'Set or clear the "may be outdated" flag on a page. The flag surfaces passively as a pill in the UI; it does not change the content. Pass stale=false to clear. Wraps POST /docs/:id/flag-stale.',
|
package/dist/tools/work-next.js
CHANGED
|
@@ -46,7 +46,6 @@ export const workNextToolConfig = {
|
|
|
46
46
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
47
47
|
};
|
|
48
48
|
export function makeWorkNextHandler(client) {
|
|
49
|
-
// Per-connection rules-hash cache, same pattern as orboto_work_start.
|
|
50
49
|
let lastKnownRulesHash;
|
|
51
50
|
return async (args, extra) => {
|
|
52
51
|
const token = mcpInstanceToken(args.agentSessionToken, extra);
|
|
@@ -64,7 +63,6 @@ export function makeWorkNextHandler(client) {
|
|
|
64
63
|
});
|
|
65
64
|
validateNextWorkEnvelope(res, args.peek);
|
|
66
65
|
if (!res.reserved) {
|
|
67
|
-
// ORB-1930 - a peek that found work: name it, reserve nothing.
|
|
68
66
|
if (args.peek && res.candidate) {
|
|
69
67
|
const c = res.candidate;
|
|
70
68
|
return {
|
|
@@ -85,8 +83,6 @@ export function makeWorkNextHandler(client) {
|
|
|
85
83
|
},
|
|
86
84
|
};
|
|
87
85
|
}
|
|
88
|
-
// ORB-1774 - a pause is an operator decision, not a backoff situation:
|
|
89
|
-
// tell the agent to idle, not to poll for a free slot.
|
|
90
86
|
if (res.reason === 'autonomy_paused') {
|
|
91
87
|
return {
|
|
92
88
|
content: [{
|
|
@@ -164,7 +160,6 @@ export function makeWorkNextHandler(client) {
|
|
|
164
160
|
}
|
|
165
161
|
}
|
|
166
162
|
}
|
|
167
|
-
// ORB-1614 - see the comment on the other fmtDeps above.
|
|
168
163
|
const fmtDeps = (edges) => edges.length === 0 ? '(none)' : edges.map((e) => `- [${e.ticketKey ?? '?'}] ${e.title ?? `External dependency (access restricted)${e.resolved ? ' - resolved' : ' - still open'}`}${e.statusName ? ` - ${e.statusName}` : ''}`).join('\n');
|
|
169
164
|
lines.push('', '## Dependencies');
|
|
170
165
|
lines.push('Blocked by:', fmtDeps(r.dependencies.blockedBy), 'Blocks:', fmtDeps(r.dependencies.blocks));
|
|
@@ -89,8 +89,6 @@ describe('orboto_work_start', () => {
|
|
|
89
89
|
expect(post.method).toBe('POST');
|
|
90
90
|
expect(post.url).toContain('/work-sessions/start');
|
|
91
91
|
expect(post.body?.ticketId).toBe('t1');
|
|
92
|
-
// The instance token must be sent unprompted, same contract as
|
|
93
|
-
// orboto_work_session_start.
|
|
94
92
|
expect(String(post.body?.agentSessionToken)).toMatch(/^mcp-/);
|
|
95
93
|
const text = res.content[0].text;
|
|
96
94
|
expect(text).toContain('Started a implementation work session on ACME-42');
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1609 - work-session tools.
|
|
3
|
-
*
|
|
4
|
-
* A work session is the transactional record behind "I am working on
|
|
5
|
-
* this ticket": it holds the lease, owns the timer, drives presence,
|
|
6
|
-
* and carries the finish-time evidence (commit + verification). It is
|
|
7
|
-
* the primitive `orboto work start / finish / next` (wave 3) is built
|
|
8
|
-
* on, and it is what makes ownership visible ACROSS accounts - a lease
|
|
9
|
-
* held by another team's bot is a 409 with the holder attached, not an
|
|
10
|
-
* invisible collision discovered at push time.
|
|
11
|
-
*
|
|
12
|
-
* The three roles that are not `implementation` deliberately do NOT
|
|
13
|
-
* reassign the ticket or move its status: a review / preflight /
|
|
14
|
-
* integration session attaches to a ticket without pretending to own
|
|
15
|
-
* its delivery. That is what makes one-ticket-one-commit workable for
|
|
16
|
-
* work that produces no commit of its own.
|
|
17
3
|
*/
|
|
18
4
|
import { z } from 'zod';
|
|
19
5
|
import { OrbotoApiError } from '../orboto-client.js';
|
|
@@ -44,12 +30,6 @@ function describe(s) {
|
|
|
44
30
|
const who = s.userFullName ?? s.userEmail ?? 'unknown';
|
|
45
31
|
return ` - ${key} [${s.role}] ${who} - lease until ${s.leaseUntil}${s.commitSha ? ` (commit ${s.commitSha.slice(0, 8)})` : ''}`;
|
|
46
32
|
}
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// orboto_work_start - ORB-1611
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Mirrors apps/mcp/src/tools/session-start.ts's local shapes - kept
|
|
51
|
-
// duplicated rather than imported, same choice that file already made for
|
|
52
|
-
// its own bundle types (no shared MCP-side schema layer for these).
|
|
53
33
|
export const workStartToolConfig = {
|
|
54
34
|
title: 'Start a work session and load the full context bundle',
|
|
55
35
|
description: 'ORB-1611 - the one-call ticket pickup. Acquires the (ticket, role) work lease with the exact same guarantees as orboto_work_session_start (exactly ONE active session per ticket+role workspace-wide, a conflict names the holder, resourceClaims apply atomically with the lease), AND in the SAME response returns the rules-hash ack (same semantics as orboto_session_start), the project primer, the ticket enriched with its description/status/priority, its checklists, its dependencies, that project\'s git connection health, and any other live sessions already on the ticket. This replaces the 8-15 separate calls (orboto_session_start, orboto_get_project_primer, orboto_get_ticket, orboto_get_checklists, orboto_list_ticket_dependencies, ...) a normal ticket pickup used to cost. Prefer this over orboto_work_session_start for picking up a ticket; use the plain tool only when you deliberately do not want the bundle (e.g. a mid-task lease renewal where you already have fresh context). A conflict never leaves a partial session behind - same rollback-on-conflict guarantee as orboto_work_session_start.',
|
|
@@ -70,12 +50,9 @@ export const workStartToolConfig = {
|
|
|
70
50
|
onConflict: z.enum(['reject', 'queue']).optional()
|
|
71
51
|
.describe('Only matters when `resourceClaims` is set. Default `reject`.'),
|
|
72
52
|
}).shape,
|
|
73
|
-
// takeover=true cancels another holder; hints describe the worst allowed call.
|
|
74
53
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
75
54
|
};
|
|
76
55
|
export function makeWorkStartHandler(client) {
|
|
77
|
-
// Per-connection rules-hash cache, mirrored from session-start.ts's
|
|
78
|
-
// makeSessionStartHandler - this closure lives for one MCP connection.
|
|
79
56
|
let lastKnownRulesHash;
|
|
80
57
|
return async (args, extra) => {
|
|
81
58
|
const token = mcpInstanceToken(args.agentSessionToken, extra);
|
|
@@ -141,9 +118,6 @@ export function makeWorkStartHandler(client) {
|
|
|
141
118
|
}
|
|
142
119
|
}
|
|
143
120
|
}
|
|
144
|
-
// ORB-1614 - a cross-project blocker/dependent the caller cannot read
|
|
145
|
-
// comes back with `title: null` - render a fixed placeholder instead
|
|
146
|
-
// of the literal "null".
|
|
147
121
|
const fmtDeps = (edges) => edges.length === 0 ? '(none)' : edges.map((e) => `- [${e.ticketKey ?? '?'}] ${e.title ?? `External dependency (access restricted)${e.resolved ? ' - resolved' : ' - still open'}`}${e.statusName ? ` - ${e.statusName}` : ''}`).join('\n');
|
|
148
122
|
lines.push('', '## Dependencies');
|
|
149
123
|
lines.push('Blocked by:', fmtDeps(res.dependencies.blockedBy), 'Blocks:', fmtDeps(res.dependencies.blocks));
|
|
@@ -216,9 +190,6 @@ export function makeWorkStartHandler(client) {
|
|
|
216
190
|
}
|
|
217
191
|
};
|
|
218
192
|
}
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
// orboto_work_session_start
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
193
|
export const workSessionStartToolConfig = {
|
|
223
194
|
title: 'Start (or renew) a work session on a ticket',
|
|
224
195
|
description: 'Take the work lease on a ticket in a given role and start its timer. This is the coordination primitive: exactly ONE active session per (ticket, role) exists workspace-wide, so a second agent attempting the same role gets a conflict naming the current holder instead of silently colliding. Re-calling with the same agent instance renews your own lease and is a no-op otherwise - safe to call defensively. Roles other than `implementation` (review / preflight / integration) attach to the ticket WITHOUT reassigning it or moving its status, so a reviewing agent no longer has to fake a claim. The lease expires on its own (default 15 min, renewed automatically by your subsequent calls), so a crashed agent never wedges a ticket. Pass `takeover: true` only when you have decided to displace the current holder - their session is closed and their time booked, and the takeover is visible in history.',
|
|
@@ -239,7 +210,6 @@ export const workSessionStartToolConfig = {
|
|
|
239
210
|
onConflict: z.enum(['reject', 'queue']).optional()
|
|
240
211
|
.describe('Only matters when `resourceClaims` is set. Default `reject`: a conflicting write claim fails the WHOLE call with the conflicting holder(s) named - if this call would have created a brand-new session, that session is rolled back rather than left holding the lease without its claims. `queue`: the conflicting claim is accepted as `state: "waiting"` instead of failing; it is promoted automatically once the conflict clears (release, finish, or the next orboto_work_sessions read).'),
|
|
241
212
|
}).shape,
|
|
242
|
-
// The optional takeover can displace someone else's active session.
|
|
243
213
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
244
214
|
};
|
|
245
215
|
export function makeWorkSessionStartHandler(client) {
|
|
@@ -280,8 +250,6 @@ export function makeWorkSessionStartHandler(client) {
|
|
|
280
250
|
if (err instanceof OrbotoApiError && err.status === 409) {
|
|
281
251
|
const claimConflicts = parseClaimConflicts(err);
|
|
282
252
|
if (claimConflicts && claimConflicts.length > 0) {
|
|
283
|
-
// ORB-1610 - a resourceClaims conflict, distinct from the lease
|
|
284
|
-
// conflict below: the body carries `claimConflicts`, not `holder`.
|
|
285
253
|
return {
|
|
286
254
|
content: [{
|
|
287
255
|
type: 'text',
|
|
@@ -293,10 +261,6 @@ export function makeWorkSessionStartHandler(client) {
|
|
|
293
261
|
isError: true,
|
|
294
262
|
};
|
|
295
263
|
}
|
|
296
|
-
// OrbotoApiError carries the raw body string; the 409 payload is the
|
|
297
|
-
// standard i18n error triple plus `holder`, which is the whole point
|
|
298
|
-
// of the conflict response - the caller must not need a second call
|
|
299
|
-
// to learn who has the ticket.
|
|
300
264
|
let holder;
|
|
301
265
|
try {
|
|
302
266
|
holder = JSON.parse(err.body).holder;
|
|
@@ -321,9 +285,6 @@ export function makeWorkSessionStartHandler(client) {
|
|
|
321
285
|
}
|
|
322
286
|
};
|
|
323
287
|
}
|
|
324
|
-
// ---------------------------------------------------------------------------
|
|
325
|
-
// orboto_work_session_finish
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
288
|
export const workSessionFinishToolConfig = {
|
|
328
289
|
title: 'Finish a work session (books time, frees the lease)',
|
|
329
290
|
description: 'End a work session: its 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.',
|
|
@@ -414,9 +375,6 @@ export function makeWorkFinishHandler(client) {
|
|
|
414
375
|
};
|
|
415
376
|
};
|
|
416
377
|
}
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
// orboto_work_sessions
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
378
|
export const workSessionsToolConfig = {
|
|
421
379
|
title: 'List live work sessions (who is working on what)',
|
|
422
380
|
description: 'The coordination view: every live work lease you can see, or the sessions on one ticket. Call this BEFORE picking up work in a fleet - a ticket with a live `implementation` lease is already being worked, and starting on it anyway is how two agents produce conflicting commits. Unlike agent presence (which reports who is online), this reports who OWNS what, across accounts.',
|
|
@@ -451,9 +409,6 @@ export function makeWorkSessionsHandler(client) {
|
|
|
451
409
|
return { content: [{ type: 'text', text }], structuredContent: { sessions: rows } };
|
|
452
410
|
};
|
|
453
411
|
}
|
|
454
|
-
// ---------------------------------------------------------------------------
|
|
455
|
-
// orboto_work_session_claims_add
|
|
456
|
-
// ---------------------------------------------------------------------------
|
|
457
412
|
export const workSessionClaimsAddToolConfig = {
|
|
458
413
|
title: 'Add resource claims to a live work session',
|
|
459
414
|
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"`.',
|
|
@@ -499,9 +454,6 @@ export function makeWorkSessionClaimsAddHandler(client) {
|
|
|
499
454
|
}
|
|
500
455
|
};
|
|
501
456
|
}
|
|
502
|
-
// ---------------------------------------------------------------------------
|
|
503
|
-
// orboto_work_session_claims_release
|
|
504
|
-
// ---------------------------------------------------------------------------
|
|
505
457
|
export const workSessionClaimsReleaseToolConfig = {
|
|
506
458
|
title: 'Release resource claims from a live work session',
|
|
507
459
|
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.',
|
|
@@ -58,8 +58,6 @@ describe('orboto_work_session_start', () => {
|
|
|
58
58
|
expect(post.method).toBe('POST');
|
|
59
59
|
expect(post.url).toContain('/work-sessions');
|
|
60
60
|
expect(post.body?.ticketId).toBe('t1');
|
|
61
|
-
// The instance token must be sent unprompted - without it the lease has
|
|
62
|
-
// no owner to renew against.
|
|
63
61
|
expect(String(post.body?.agentSessionToken)).toMatch(/^mcp-/);
|
|
64
62
|
const text = res.content[0].text;
|
|
65
63
|
expect(text).toContain('Started a implementation work session on ACME-42');
|
|
@@ -112,7 +110,6 @@ describe('orboto_work_session_start', () => {
|
|
|
112
110
|
});
|
|
113
111
|
const post = calls[calls.length - 1];
|
|
114
112
|
expect(post.body).toMatchObject({ role: 'review', leaseSeconds: 3600, takeover: true });
|
|
115
|
-
// A review session attaches without booking time by default.
|
|
116
113
|
expect(res.content[0].text).toContain('No timer started for this role.');
|
|
117
114
|
});
|
|
118
115
|
});
|
|
@@ -350,8 +347,6 @@ describe('ORB-1610 - orboto_work_session_claims_release', () => {
|
|
|
350
347
|
it('releases everything when claims is omitted, and still sends a body', async () => {
|
|
351
348
|
const calls = stub([{ text: JSON.stringify({ ...SESSION, resourceClaims: [] }) }]);
|
|
352
349
|
const res = await makeWorkSessionClaimsReleaseHandler(client)({ sessionId: 'ws1' });
|
|
353
|
-
// A DELETE with a genuinely absent body 400s on the API's schema
|
|
354
|
-
// (ORB-1610 finding) - the handler must always send at least `{}`.
|
|
355
350
|
expect(calls[0].body).toEqual({});
|
|
356
351
|
expect(res.content[0].text).toContain('Released every claim');
|
|
357
352
|
});
|
package/dist/toolset.js
CHANGED
|
@@ -2,100 +2,33 @@
|
|
|
2
2
|
* ORB-1520 - manifest triage for the Code-Mode escape hatch (epic
|
|
3
3
|
* ORB-1517).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* tool schema at connect time - with ~190 named tools that is tens of
|
|
7
|
-
* thousands of tokens before the user says a word. The default manifest
|
|
8
|
-
* is therefore the CURATED set below; the long tail stays fully
|
|
9
|
-
* reachable through `orboto_api_search` + `orboto_api_call`, and the
|
|
10
|
-
* complete named manifest is one opt-in away - and since ORB-1805 a
|
|
11
|
-
* SMALLER one is too, for local models with an 8k/16k window:
|
|
12
|
-
*
|
|
13
|
-
* stdio: ORBOTO_MCP_TOOLSET=minimal | curated | full
|
|
14
|
-
* http: .../mcp?toolset=minimal (or header x-orboto-toolset: full;
|
|
15
|
-
* the query param rides on every request, so no session
|
|
16
|
-
* persistence is involved)
|
|
17
|
-
*
|
|
18
|
-
* KEEP-LIST BASIS - measured, not guessed (ORB-1520): 30-day
|
|
19
|
-
* `mcp_call_log` distribution on the core instance, 2026-08-10, 15.5k
|
|
20
|
-
* calls total. Every tool with a meaningful call volume is in; the two
|
|
21
|
-
* additions beyond the data are `orboto_my_tickets` (the natural "what
|
|
22
|
-
* am I working on" entry for interactive desktop clients - the exact
|
|
23
|
-
* audience this manifest serves; agent-dominated prod traffic
|
|
24
|
-
* under-counts it) and the escape-hatch pair itself. The numbers live
|
|
25
|
-
* in the ORB-1520 ticket comment.
|
|
26
|
-
*
|
|
27
|
-
* Tail tools are NOT deleted: their code, tests, chat mirrors and
|
|
28
|
-
* drift-guard entries are untouched - only their REGISTRATION is
|
|
29
|
-
* skipped in curated mode (the chat-tools parity guard reads server.ts
|
|
30
|
-
* SOURCE, so it keeps covering the whole set in both modes).
|
|
5
|
+
* @see ORB-1805
|
|
31
6
|
*/
|
|
32
7
|
/**
|
|
33
8
|
* ORB-1805 - the small-context tier.
|
|
34
9
|
*
|
|
35
|
-
*
|
|
36
|
-
* tool schemas plus ~1k of instructions, so an 8k local model (LM Studio
|
|
37
|
-
* / Ollama defaults) dies before the first user turn:
|
|
38
|
-
* `n_keep: 13533 >= n_ctx: 8192`. `minimal` is the daily loop and
|
|
39
|
-
* nothing else - orient, find, read, write, claim, close, track time -
|
|
40
|
-
* plus the escape-hatch pair, which keeps the ENTIRE REST API reachable
|
|
41
|
-
* from 12 tools.
|
|
42
|
-
*
|
|
43
|
-
* The pair is `claim` + `close_ticket` because that is the loop the
|
|
44
|
-
* skill's own workflow mandates (SKILL.md "Claim = assign + status +
|
|
45
|
-
* timer; stop the timer the moment you finish"), not
|
|
46
|
-
* `work_next`/`work_finish` (those are not even in the curated set).
|
|
47
|
-
*
|
|
48
|
-
* Deliberately OUT, and why the loop still holds without them:
|
|
49
|
-
* - `orboto_whoami` - measured at 357 tokens, 12 % of the whole budget,
|
|
50
|
-
* for data `orboto_session_start` already prints ("You are <name>
|
|
51
|
-
* (<email>)." + the workspace write language). The mandated first call
|
|
52
|
-
* covers it; a second identity tool does not earn a tier this small.
|
|
53
|
-
* - `orboto_check_similar` - `orboto_create_ticket` enforces the
|
|
54
|
-
* duplicate block server-side (ORB-1471), so the mandated pre-create
|
|
55
|
-
* check still happens, just inside the create.
|
|
56
|
-
* - `orboto_my_tickets` / `orboto_list_tickets` - `orboto_session_start`
|
|
57
|
-
* already returns the caller's in-progress work, and `orboto_search`
|
|
58
|
-
* covers "anything about X".
|
|
59
|
-
* - `orboto_move_ticket` - claim moves to in_progress and close moves to
|
|
60
|
-
* done; any other transition goes through `orboto_api_call`.
|
|
61
|
-
* - `orboto_help` / `orboto_response_expand` - both are recovery paths
|
|
62
|
-
* for large payloads, which is the opposite of this tier's problem.
|
|
63
|
-
*
|
|
64
|
-
* MUST stay a subset of CURATED_TOOLS (asserted in toolset.test.ts): a
|
|
65
|
-
* name that is not registered in curated cannot be registered here
|
|
66
|
-
* either, and the tier ordering minimal < curated < full is what the
|
|
67
|
-
* docs promise.
|
|
10
|
+
* @see ORB-1471
|
|
68
11
|
*/
|
|
69
12
|
export const MINIMAL_TOOLS = new Set([
|
|
70
|
-
// Orient (session_start also reports who you are + the write language)
|
|
71
13
|
'orboto_session_start',
|
|
72
|
-
// Find + read
|
|
73
14
|
'orboto_search',
|
|
74
15
|
'orboto_get_ticket',
|
|
75
|
-
// Write
|
|
76
16
|
'orboto_create_ticket',
|
|
77
17
|
'orboto_update_ticket',
|
|
78
18
|
'orboto_comment',
|
|
79
|
-
// The claim -> close loop + its timer
|
|
80
19
|
'orboto_claim',
|
|
81
20
|
'orboto_close_ticket',
|
|
82
21
|
'orboto_timer_start',
|
|
83
22
|
'orboto_timer_stop',
|
|
84
|
-
// Everything else, on demand (ORB-1959 measured orboto_messages at ~3k
|
|
85
|
-
// chars - a fifth of this tier's whole window - so the inbox stays in
|
|
86
|
-
// curated, where the default connection lives; minimal is the explicit
|
|
87
|
-
// small-model opt-in and reaches the inbox through orboto_api_call).
|
|
88
23
|
'orboto_api_search',
|
|
89
24
|
'orboto_api_call',
|
|
90
25
|
]);
|
|
91
26
|
/** Order matches the measured 30d call counts (see ticket comment). */
|
|
92
27
|
export const CURATED_TOOLS = new Set([
|
|
93
|
-
// Session + identity + orientation
|
|
94
28
|
'orboto_session_start', // 662 calls/30d, and the binding-rules bootstrap
|
|
95
29
|
'orboto_whoami', // 28
|
|
96
30
|
'orboto_get_project_primer', // 131
|
|
97
31
|
'orboto_list_projects', // 27, referenced by the server instructions
|
|
98
|
-
// Reads
|
|
99
32
|
'orboto_get_ticket', // 1764
|
|
100
33
|
'orboto_list_tickets', // 161
|
|
101
34
|
'orboto_my_tickets', // interactive-client staple (see header)
|
|
@@ -103,7 +36,6 @@ export const CURATED_TOOLS = new Set([
|
|
|
103
36
|
'orboto_query', // 26 - the OQL surface, prior art of this pattern
|
|
104
37
|
'orboto_check_similar', // 258 - mandated pre-create dup check
|
|
105
38
|
'orboto_get_doc', // 86
|
|
106
|
-
// Ticket writes (the strict claim->commit->close loop)
|
|
107
39
|
'orboto_create_ticket', // 2069
|
|
108
40
|
'orboto_bulk_create_tickets', // ORB-1694 - 48 measured runs of consecutive creates
|
|
109
41
|
'orboto_bulk_add_ticket_dependencies', // ORB-1694 - longest dependency run: 27 calls
|
|
@@ -116,26 +48,15 @@ export const CURATED_TOOLS = new Set([
|
|
|
116
48
|
'orboto_add_ticket_dependency', // 314 - the mandated blocker primitive
|
|
117
49
|
'orboto_check', // 103 - acceptance-criteria checklists
|
|
118
50
|
'orboto_create_milestone', // 42 - "milestone FIRST" rule for big features
|
|
119
|
-
// Time tracking (the timer rules)
|
|
120
51
|
'orboto_timer_start', // 34
|
|
121
52
|
'orboto_timer_stop', // 772
|
|
122
53
|
'orboto_log_time', // 130
|
|
123
54
|
'orboto_get_timer', // 43
|
|
124
|
-
// ORB-1959 - agent coordination on the DEFAULT connection. Without
|
|
125
|
-
// these four an agent on curated could neither receive nor acknowledge
|
|
126
|
-
// mail (broadcasts included, they land in the inbox since ORB-1959),
|
|
127
|
-
// announce itself (heartbeat / presence) nor reach another agent - the
|
|
128
|
-
// workspace rules demand exactly that, and every fresh `mcp add` forgot
|
|
129
|
-
// the ?toolset=full workaround. Four small schemas; measured cost in
|
|
130
|
-
// manifest-size.test.ts.
|
|
131
55
|
'orboto_messages',
|
|
132
56
|
'orboto_agent_notify',
|
|
133
57
|
'orboto_agent_broadcast',
|
|
134
58
|
'orboto_agent_heartbeat',
|
|
135
|
-
// Transport plumbing + the escape hatch itself
|
|
136
59
|
'orboto_response_expand',
|
|
137
|
-
// ORB-1741 - the way back from one-line manifest summaries: full
|
|
138
|
-
// guidance on demand. Must ride every manifest the summaries ride.
|
|
139
60
|
'orboto_report_feedback', // ORB-1910 - an agent that hits a bug reports it without ?toolset=full
|
|
140
61
|
'orboto_help', // ORB-1697 - the way back from a budget cut
|
|
141
62
|
'orboto_api_search', // ORB-1518 - discovery half of the escape hatch
|
package/dist/toolset.test.js
CHANGED
|
@@ -7,8 +7,6 @@ import { buildOrbotoMcpServer } from './server.js';
|
|
|
7
7
|
import { CURATED_TOOLS, MINIMAL_TOOLS, resolveToolset, toolInToolset } from './toolset.js';
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
vi.restoreAllMocks();
|
|
10
|
-
// buildOrbotoMcpServer fetches /agent-instructions at connect; offline
|
|
11
|
-
// in unit tests - the catch keeps the fallback rules, which is fine.
|
|
12
10
|
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('offline test'));
|
|
13
11
|
delete process.env.ORBOTO_MCP_TOOLSET;
|
|
14
12
|
});
|
|
@@ -17,9 +15,6 @@ afterEach(() => {
|
|
|
17
15
|
delete process.env.ORBOTO_MCP_TOOLSET;
|
|
18
16
|
});
|
|
19
17
|
function registeredTools(server) {
|
|
20
|
-
// The SDK keeps its registry on `_registeredTools` (verified against
|
|
21
|
-
// @modelcontextprotocol/sdk 1.29). If a bump renames it, this helper
|
|
22
|
-
// fails loudly rather than passing on an empty object.
|
|
23
18
|
const tools = server._registeredTools;
|
|
24
19
|
if (!tools || Object.keys(tools).length === 0) {
|
|
25
20
|
throw new Error('SDK _registeredTools not found or empty - did the SDK internals change?');
|
|
@@ -37,9 +32,6 @@ describe('resolveToolset', () => {
|
|
|
37
32
|
expect(resolveToolset('bogus', 'full')).toBe('full');
|
|
38
33
|
expect(resolveToolset('bogus', 'nonsense')).toBe('curated');
|
|
39
34
|
});
|
|
40
|
-
// ORB-1805 - the small-context tier resolves on both surfaces and,
|
|
41
|
-
// like the others, a typo still degrades to curated rather than
|
|
42
|
-
// silently handing an 8k model 175 tools.
|
|
43
35
|
it('minimal resolves from either surface and keeps the safe fallback', () => {
|
|
44
36
|
expect(resolveToolset('minimal', undefined)).toBe('minimal');
|
|
45
37
|
expect(resolveToolset(undefined, 'minimal')).toBe('minimal');
|
|
@@ -54,7 +46,6 @@ describe('toolInToolset', () => {
|
|
|
54
46
|
expect(toolInToolset('orboto_claim', 'curated')).toBe(true);
|
|
55
47
|
expect(toolInToolset('orboto_api_call', 'curated')).toBe(true);
|
|
56
48
|
});
|
|
57
|
-
// ORB-1959 - coordination is part of the default connection, not an opt-in.
|
|
58
49
|
it('curated carries the four agent-coordination tools; minimal stays the 12-tool loop', () => {
|
|
59
50
|
for (const name of ['orboto_messages', 'orboto_agent_notify', 'orboto_agent_broadcast', 'orboto_agent_heartbeat']) {
|
|
60
51
|
expect(toolInToolset(name, 'curated'), name).toBe(true);
|
|
@@ -65,7 +56,6 @@ describe('toolInToolset', () => {
|
|
|
65
56
|
expect(toolInToolset('orboto_claim', 'minimal')).toBe(true);
|
|
66
57
|
expect(toolInToolset('orboto_api_search', 'minimal')).toBe(true);
|
|
67
58
|
expect(toolInToolset('orboto_api_call', 'minimal')).toBe(true);
|
|
68
|
-
// In curated, out of minimal.
|
|
69
59
|
expect(toolInToolset('orboto_list_projects', 'minimal')).toBe(false);
|
|
70
60
|
expect(toolInToolset('orboto_help', 'minimal')).toBe(false);
|
|
71
61
|
expect(toolInToolset('orboto_wiki_ask', 'minimal')).toBe(false);
|
|
@@ -73,10 +63,6 @@ describe('toolInToolset', () => {
|
|
|
73
63
|
});
|
|
74
64
|
describe('ORB-1805 - the tiers nest', () => {
|
|
75
65
|
it('MINIMAL_TOOLS is a strict subset of CURATED_TOOLS', () => {
|
|
76
|
-
// A name that is not registered in curated cannot register in
|
|
77
|
-
// minimal either - the tier ordering minimal < curated < full is
|
|
78
|
-
// what the docs promise, and a typo here would silently ship an
|
|
79
|
-
// empty slot.
|
|
80
66
|
const notInCurated = [...MINIMAL_TOOLS].filter((t) => !CURATED_TOOLS.has(t));
|
|
81
67
|
expect(notInCurated).toEqual([]);
|
|
82
68
|
expect(MINIMAL_TOOLS.size).toBeLessThan(CURATED_TOOLS.size);
|
|
@@ -115,9 +101,6 @@ describe('buildOrbotoMcpServer toolset gating', () => {
|
|
|
115
101
|
it('curated instructions point at the escape hatch; full keeps get_checklists', async () => {
|
|
116
102
|
const curated = await buildOrbotoMcpServer(baseOpts);
|
|
117
103
|
const full = await buildOrbotoMcpServer({ ...baseOpts, toolset: 'full' });
|
|
118
|
-
// `_instructions` verified against @modelcontextprotocol/sdk 1.29
|
|
119
|
-
// (server/index.js). Assert non-empty first so an SDK rename fails
|
|
120
|
-
// this test loudly instead of green-skipping.
|
|
121
104
|
const instructionsOf = (s) => String(s.server._instructions ?? '');
|
|
122
105
|
const curatedText = instructionsOf(curated);
|
|
123
106
|
const fullText = instructionsOf(full);
|
|
@@ -137,12 +120,9 @@ describe('buildOrbotoMcpServer toolset gating', () => {
|
|
|
137
120
|
const minimal = await buildOrbotoMcpServer({ ...baseOpts, toolset: 'minimal' });
|
|
138
121
|
const text = String(minimal.server._instructions ?? '');
|
|
139
122
|
expect(text.length).toBeGreaterThan(100);
|
|
140
|
-
// The two things a minimal-tier agent cannot work without.
|
|
141
123
|
expect(text).toContain('orboto_session_start');
|
|
142
124
|
expect(text).toContain('orboto_api_search');
|
|
143
125
|
expect(text).toContain('MINIMAL');
|
|
144
|
-
// ORB-1805 - the workspace rules block is deliberately absent; it
|
|
145
|
-
// arrives via orboto_session_start, on the caller's budget.
|
|
146
126
|
expect(text).not.toContain('Working rules for this workspace:');
|
|
147
127
|
});
|
|
148
128
|
it('minimal drops the display title; curated keeps it', async () => {
|
|
@@ -15,7 +15,6 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { VERSION } from './version.js';
|
|
16
16
|
const SRC = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const pkg = createRequire(import.meta.url)('../package.json');
|
|
18
|
-
// version.ts legitimately holds the '0.0.0' runtime fallback literal.
|
|
19
18
|
const ALLOWED = new Set(['version.ts']);
|
|
20
19
|
function walk(dir, acc = []) {
|
|
21
20
|
for (const entry of readdirSync(dir)) {
|
|
@@ -35,9 +34,6 @@ describe('ORB-1173 - version literal guard', () => {
|
|
|
35
34
|
});
|
|
36
35
|
it('no hardcoded semver string literal in src (use VERSION instead)', () => {
|
|
37
36
|
const offenders = [];
|
|
38
|
-
// A quoted X.Y.Z literal - what a drifting hardcoded version looks
|
|
39
|
-
// like. Comments (// v0.110.1) and ticket refs (ORB-1166) are not
|
|
40
|
-
// quoted semver, so they don't trip this.
|
|
41
37
|
const semverLiteral = /['"]\d+\.\d+\.\d+['"]/;
|
|
42
38
|
for (const file of walk(SRC)) {
|
|
43
39
|
const text = readFileSync(file, 'utf8');
|
package/dist/version.js
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1166 - single source of truth for the MCP server version.
|
|
3
|
-
*
|
|
4
|
-
* Read from package.json (which release.mjs bumps) so serverInfo.version
|
|
5
|
-
* + the User-Agent header always reflect the real release instead of a
|
|
6
|
-
* hardcoded literal that drifts.
|
|
7
|
-
*
|
|
8
|
-
* WRAPPED IN try/catch on purpose: the Docker runtime image is stripped
|
|
9
|
-
* to `dist/` + `node_modules/` and may not ship package.json at the path
|
|
10
|
-
* `createRequire` resolves. A bare `require('../package.json')` at module
|
|
11
|
-
* load threw there and crash-looped the MCP container (took prod down in
|
|
12
|
-
* v0.110.1). The catch guarantees this module can NEVER crash the server
|
|
13
|
-
* on boot; the Dockerfile now also copies package.json so the real
|
|
14
|
-
* version resolves in-container.
|
|
15
3
|
*/
|
|
16
4
|
import { createRequire } from 'node:module';
|
|
17
5
|
function readVersion() {
|
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
* ORB-1738 - declared output schemas ADVERTISE the response budget's
|
|
3
3
|
* `__truncation` marker.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* into structuredContent, the SDK advertises output schemas with
|
|
7
|
-
* additionalProperties:false, and strict clients validate structured
|
|
8
|
-
* content against that - so every over-budget response of a tool with a
|
|
9
|
-
* declared outputSchema was rejected wholesale (orboto_list_projects
|
|
10
|
-
* failed on EVERY call of this 57-project workspace; reported via the
|
|
11
|
-
* agent inbox 2026-08-28).
|
|
5
|
+
* @see ORB-1733
|
|
12
6
|
*/
|
|
13
7
|
import { describe, it, expect } from 'vitest';
|
|
14
8
|
import { listProjectsToolConfig } from './tools/list-projects.js';
|
|
@@ -40,19 +34,13 @@ describe('ORB-1738 - __truncation is part of the advertised output schema', () =
|
|
|
40
34
|
};
|
|
41
35
|
const { result } = applyResponseBudget('orboto_list_projects', bigResult);
|
|
42
36
|
const sc = result.structuredContent;
|
|
43
|
-
// The budget DID cut and marked it.
|
|
44
37
|
expect(sc.__truncation).toBeDefined();
|
|
45
38
|
const shape = advertisedOutputShape(listProjectsToolConfig.outputSchema);
|
|
46
|
-
// Before the fix this was false - the marker was undeclared.
|
|
47
39
|
expect(strictClientAccepts(shape, sc)).toBe(true);
|
|
48
|
-
// And the marker itself matches BOTH the exact runtime shape and the
|
|
49
|
-
// compact schema actually advertised on the wire (ORB-1805).
|
|
50
40
|
expect(TruncationBlockSchema.safeParse(sc.__truncation).success).toBe(true);
|
|
51
41
|
expect(TruncationBlockAdvertisedSchema.safeParse(sc.__truncation).success).toBe(true);
|
|
52
42
|
});
|
|
53
43
|
it('ORB-1805 - the advertised marker schema is strictly more permissive than the exact one', () => {
|
|
54
|
-
// Anything the exact shape accepts, the advertised one must accept -
|
|
55
|
-
// otherwise the diet would start rejecting valid payloads.
|
|
56
44
|
const block = {
|
|
57
45
|
handle: 'h1', budgetChars: 4000, originalChars: 9000, omittedChars: 5000,
|
|
58
46
|
omitted: [{ path: 'items', kind: 'array', omittedItems: 12, keptItems: 3 }],
|
|
@@ -60,7 +48,6 @@ describe('ORB-1738 - __truncation is part of the advertised output schema', () =
|
|
|
60
48
|
};
|
|
61
49
|
expect(TruncationBlockSchema.safeParse(block).success).toBe(true);
|
|
62
50
|
expect(TruncationBlockAdvertisedSchema.safeParse(block).success).toBe(true);
|
|
63
|
-
// A future field added to the runtime block must not break clients.
|
|
64
51
|
expect(TruncationBlockAdvertisedSchema.safeParse({ ...block, futureField: 1 }).success).toBe(true);
|
|
65
52
|
});
|
|
66
53
|
it('the budget layer adds exactly ONE key - __truncation - and nothing else (class sweep)', () => {
|