@orboto/mcp 0.193.0 → 0.194.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/event-bridge.js +1 -47
  2. package/dist/event-bridge.test.js +0 -19
  3. package/dist/http-transport-binding.test.js +1 -4
  4. package/dist/http-transport.js +1 -213
  5. package/dist/http-transport.test.js +0 -56
  6. package/dist/index.js +1 -48
  7. package/dist/input-schema.js +2 -76
  8. package/dist/input-schema.test.js +1 -15
  9. package/dist/instructions-budget.test.js +0 -2
  10. package/dist/manifest-size.test.js +3 -72
  11. package/dist/oauth-bootstrap.js +0 -47
  12. package/dist/oauth-bootstrap.test.js +4 -16
  13. package/dist/orboto-client.js +0 -30
  14. package/dist/orboto-client.test.js +0 -12
  15. package/dist/prompts.js +0 -39
  16. package/dist/protect-text-usage.test.js +1 -9
  17. package/dist/required-rules-wrapper.test.js +0 -5
  18. package/dist/required-rules.js +0 -2
  19. package/dist/resources.js +0 -62
  20. package/dist/resources.test.js +0 -16
  21. package/dist/response-budget.js +2 -150
  22. package/dist/response-budget.test.js +1 -32
  23. package/dist/server.js +4 -180
  24. package/dist/session-nudge.js +0 -9
  25. package/dist/session-nudge.test.js +2 -10
  26. package/dist/session-nudge.transport.test.js +0 -23
  27. package/dist/tool-annotations.test.js +0 -44
  28. package/dist/tool-docs.js +1 -12
  29. package/dist/tool-docs.test.js +2 -8
  30. package/dist/tool-parameters.test.js +0 -19
  31. package/dist/tools/absence-writes.js +1 -17
  32. package/dist/tools/admin-translations.js +0 -11
  33. package/dist/tools/admin-writes.js +0 -28
  34. package/dist/tools/admin-writes.test.js +0 -1
  35. package/dist/tools/agent-coordination.js +2 -24
  36. package/dist/tools/agent-coordination.test.js +0 -2
  37. package/dist/tools/agent-instructions.js +0 -10
  38. package/dist/tools/agent-instructions.test.js +0 -2
  39. package/dist/tools/agent-messages.js +0 -1
  40. package/dist/tools/ai-status.js +0 -14
  41. package/dist/tools/ai-usage.js +0 -10
  42. package/dist/tools/analytics.js +0 -2
  43. package/dist/tools/api-call.js +0 -12
  44. package/dist/tools/api-call.test.js +1 -2
  45. package/dist/tools/api-search.js +1 -6
  46. package/dist/tools/approvals.js +0 -6
  47. package/dist/tools/attach.js +0 -25
  48. package/dist/tools/backup.js +0 -13
  49. package/dist/tools/backup.test.js +1 -2
  50. package/dist/tools/bulk-create.js +1 -25
  51. package/dist/tools/bulk-create.test.js +0 -4
  52. package/dist/tools/bulk-writes.js +69 -61
  53. package/dist/tools/bulk-writes.test.js +34 -4
  54. package/dist/tools/check-similar.js +0 -17
  55. package/dist/tools/checklist-writes.js +82 -27
  56. package/dist/tools/checklist-writes.test.js +69 -1
  57. package/dist/tools/claim.js +1 -71
  58. package/dist/tools/claim.test.js +0 -2
  59. package/dist/tools/critical-path.js +0 -5
  60. package/dist/tools/critical-path.test.js +0 -3
  61. package/dist/tools/cross-project-links.ee.js +1 -24
  62. package/dist/tools/doc-attachments.js +0 -28
  63. package/dist/tools/doc-attachments.test.js +1 -4
  64. package/dist/tools/doc-comments.js +1 -37
  65. package/dist/tools/doc-comments.test.js +1 -2
  66. package/dist/tools/doc-edits.js +1 -31
  67. package/dist/tools/doc-edits.test.js +0 -9
  68. package/dist/tools/doc-export.js +0 -24
  69. package/dist/tools/doc-export.test.js +0 -12
  70. package/dist/tools/doc-revisions.js +0 -23
  71. package/dist/tools/docs-ai.js +0 -34
  72. package/dist/tools/docs-ai.test.js +0 -11
  73. package/dist/tools/docs.js +1 -75
  74. package/dist/tools/docs.test.js +1 -3
  75. package/dist/tools/embedding-status.js +0 -13
  76. package/dist/tools/embedding-status.test.js +0 -1
  77. package/dist/tools/feedback.js +1 -15
  78. package/dist/tools/feedback.test.js +0 -1
  79. package/dist/tools/get-checklists.js +6 -14
  80. package/dist/tools/get-project-primer.js +1 -25
  81. package/dist/tools/get-project.js +0 -17
  82. package/dist/tools/get-ticket.js +1 -61
  83. package/dist/tools/get-timer.js +0 -11
  84. package/dist/tools/git-health-reasons.js +0 -1
  85. package/dist/tools/identity.js +0 -13
  86. package/dist/tools/list-projects.js +0 -20
  87. package/dist/tools/list-projects.test.js +0 -1
  88. package/dist/tools/list-tickets.js +10 -9
  89. package/dist/tools/milestones-crud.test.js +0 -4
  90. package/dist/tools/milestones.js +0 -43
  91. package/dist/tools/my-tickets.js +0 -7
  92. package/dist/tools/phase-b-tools.test.js +14 -21
  93. package/dist/tools/primer-facts.js +1 -49
  94. package/dist/tools/primer-facts.test.js +0 -20
  95. package/dist/tools/project-listings.js +0 -25
  96. package/dist/tools/query.js +1 -12
  97. package/dist/tools/query.test.js +0 -8
  98. package/dist/tools/raci.js +0 -11
  99. package/dist/tools/requirements-spec.js +1 -12
  100. package/dist/tools/response-expand.js +0 -19
  101. package/dist/tools/review-policy.js +0 -24
  102. package/dist/tools/review-policy.test.js +0 -2
  103. package/dist/tools/search.js +1 -13
  104. package/dist/tools/session-start.js +1 -136
  105. package/dist/tools/session-start.test.js +1 -44
  106. package/dist/tools/set-parent.js +1 -19
  107. package/dist/tools/shared.js +0 -41
  108. package/dist/tools/shared.test.js +0 -4
  109. package/dist/tools/similar-projection.js +0 -15
  110. package/dist/tools/ticket-attachments.js +1 -25
  111. package/dist/tools/ticket-schedules.js +0 -9
  112. package/dist/tools/ticket-schedules.test.js +0 -1
  113. package/dist/tools/ticket-writes.js +1 -110
  114. package/dist/tools/ticket-writes.test.js +1 -15
  115. package/dist/tools/time-writes.js +26 -46
  116. package/dist/tools/time-writes.test.js +18 -0
  117. package/dist/tools/update-project.js +1 -27
  118. package/dist/tools/update-project.test.js +1 -1
  119. package/dist/tools/wiki.js +0 -8
  120. package/dist/tools/work-next.js +0 -5
  121. package/dist/tools/work-sessions-start.test.js +0 -2
  122. package/dist/tools/work-sessions.js +0 -48
  123. package/dist/tools/work-sessions.test.js +0 -5
  124. package/dist/toolset.js +2 -81
  125. package/dist/toolset.test.js +0 -20
  126. package/dist/version-guard.test.js +0 -4
  127. package/dist/version.js +0 -12
  128. package/dist/with-metrics-output-schema.test.js +1 -14
  129. package/dist/with-metrics.js +2 -74
  130. package/dist/with-metrics.test.js +7 -29
  131. package/package.json +1 -1
@@ -31,9 +31,6 @@ function stub(responses) {
31
31
  ok: r.ok ?? true,
32
32
  status: r.status ?? 200,
33
33
  statusText: 'OK',
34
- // `'json' in r` so an explicit `json: null` preserves null;
35
- // `r.json ?? {}` would collapse null → {} and hide nullable
36
- // endpoints (e.g. GET /time/timer returns the row or null).
37
34
  json: async () => ('json' in r ? r.json : {}),
38
35
  text: async () => '',
39
36
  };
@@ -48,7 +45,6 @@ describe('orboto_get_project', () => {
48
45
  { json: PROJ },
49
46
  { json: [{ id: 'm1', name: 'v1', status: 'active', startDate: '2026-01-01', endDate: '2026-03-01' }] },
50
47
  { json: [{ id: 'l1', name: 'bug', color: '#f00' }] },
51
- // Members endpoint response: nested user + role objects per the real schema.
52
48
  { json: [{ userId: 'u1', projectId: 'p1', roleId: 'r1',
53
49
  user: { id: 'u1', email: 'ada@acme', fullName: 'Ada', avatarUrl: null },
54
50
  role: { id: 'r1', name: 'developer' } }] },
@@ -75,8 +71,19 @@ describe('orboto_list_tickets', () => {
75
71
  expect(calls[1]).toContain('statusCategory=in_progress');
76
72
  expect(calls[1]).toContain('limit=10');
77
73
  });
78
- // ORB-1696 - the shared resolver contract: key, name, OR UUID; an
79
- // ambiguous name errors listing candidates. One test per form.
74
+ it('passes unscheduled=true for the backlog', async () => {
75
+ const calls = stub([
76
+ { json: PROJ },
77
+ { json: { items: [], nextCursor: null } },
78
+ ]);
79
+ await makeListTicketsHandler(client)({ projectKey: 'ACME', unscheduled: true, statusCategory: 'todo' });
80
+ expect(calls[1]).toContain('unscheduled=true');
81
+ expect(calls[1]).toContain('statusCategory=todo');
82
+ });
83
+ it('refuses unscheduled together with milestone', async () => {
84
+ stub([{ json: PROJ }]);
85
+ await expect(makeListTicketsHandler(client)({ projectKey: 'ACME', unscheduled: true, milestone: 'ACME-M1' })).rejects.toThrow(/not both/);
86
+ });
80
87
  it('resolves milestone KEY (ORB-1696)', async () => {
81
88
  const calls = stub([
82
89
  { json: PROJ },
@@ -133,7 +140,6 @@ describe('orboto_list_tickets', () => {
133
140
  ]);
134
141
  await expect(makeListTicketsHandler(client)({ projectKey: 'ACME', milestone: 'ghost' })).rejects.toThrow(/Milestone "ghost" not found/);
135
142
  });
136
- // ORB-1605 - the stalled-ingestion signal passes through into structuredContent.
137
143
  it('surfaces waitingForGitIngestion per ticket', async () => {
138
144
  stub([
139
145
  { json: PROJ },
@@ -157,8 +163,6 @@ describe('orboto_list_tickets', () => {
157
163
  });
158
164
  });
159
165
  describe('orboto_get_ticket', () => {
160
- // Fixture: empty children page (children METADATA is always fetched; the
161
- // rows only surface with include: ["children"] since ORB-1698).
162
166
  const NO_CHILDREN = { json: { items: [], nextCursor: null } };
163
167
  it('ORB-1698 default card: only enriched + children + attachments are fetched - comments/checklists/git untouched', async () => {
164
168
  const calls = stub([
@@ -174,7 +178,6 @@ describe('orboto_get_ticket', () => {
174
178
  expect(calls.some((c) => c.includes('/checklists'))).toBe(false);
175
179
  expect(calls.some((c) => c.includes('/git-activity'))).toBe(false);
176
180
  const sc = res.structuredContent;
177
- // Counts always present; bodies absent by default - never silently.
178
181
  expect(sc.commentCount).toBe(3);
179
182
  expect(sc.checklistProgress).toEqual({ done: 1, total: 2 });
180
183
  expect(sc.attachmentCount).toBe(0);
@@ -230,8 +233,6 @@ describe('orboto_get_ticket', () => {
230
233
  expect(sc.commentsHasMore).toBe(true);
231
234
  expect(sc.parentTicket).toBeNull();
232
235
  });
233
- // ORB-1605 - the stalled-ingestion signal surfaces in both the header
234
- // text and structuredContent.
235
236
  it('surfaces waitingForGitIngestion in the header and structured content', async () => {
236
237
  stub([
237
238
  { json: PROJ },
@@ -279,9 +280,7 @@ describe('orboto_get_ticket', () => {
279
280
  stub([
280
281
  { json: PROJ },
281
282
  { json: { id: 't5', projectId: 'p1', ticketKey: 'ACME-5', title: 'Phase A', status: 'DONE', statusName: 'Done', statusCategory: 'done', type: 'task', priority: 'normal', gitActivityCount: 0, parentTicketId: 't1' } },
282
- // Enriched by-id refetch
283
283
  { json: { id: 't5', projectId: 'p1', ticketKey: 'ACME-5', title: 'Phase A', status: 'DONE', statusName: 'Done', statusCategory: 'done', type: 'task', priority: 'normal' } },
284
- // Parent ticket fetch
285
284
  { json: { id: 't1', projectId: 'p1', ticketKey: 'ACME-1', title: 'Epic foo', status: 'IN_PROGRESS', statusName: 'In Progress', statusCategory: 'in_progress' } },
286
285
  NO_CHILDREN,
287
286
  { json: [] },
@@ -316,10 +315,7 @@ describe('orboto_get_ticket', () => {
316
315
  it('ORB-1023: surfaces milestone (name, not UUID) + statusCategory from the enriched by-id refetch', async () => {
317
316
  stub([
318
317
  { json: PROJ },
319
- // Bare by-key resolver row: milestoneId set, but no milestoneName /
320
- // statusCategory - this is the shape that dropped the milestone.
321
318
  { json: { id: 't1', projectId: 'p1', ticketKey: 'ACME-9', title: 'Bug', status: 'DONE', milestoneId: 'm1', gitActivityCount: 0, parentTicketId: null } },
322
- // Enriched by-id refetch: carries statusCategory + the resolved name.
323
319
  { json: { id: 't1', projectId: 'p1', ticketKey: 'ACME-9', title: 'Bug', status: 'DONE', statusName: 'Done', statusCategory: 'done', type: 'bug', priority: 'normal', milestoneId: 'm1', milestoneName: 'Sprint 7' } },
324
320
  NO_CHILDREN,
325
321
  { json: [] },
@@ -408,7 +404,6 @@ describe('milestone tools', () => {
408
404
  { json: { total: 12, byStatus: { TODO: 12 } } },
409
405
  ]);
410
406
  const res = await makeGetMilestoneHandler(client)({ projectKey: 'ACME', milestone: 'acme-m19' });
411
- // The resolver spans closed milestones too.
412
407
  expect(calls[1]).toContain('includeClosed=true');
413
408
  const sc = res.structuredContent;
414
409
  expect(sc.milestone.name).toBe('Phase 35');
@@ -478,7 +473,6 @@ describe('orboto_get_checklists', () => {
478
473
  id: 'cl1', title: 'Before ship', triggersDone: true,
479
474
  progress: { done: 1, total: 2 },
480
475
  items: [
481
- // One plain item, one linked-to-another-ticket item.
482
476
  { id: 'i1', content: 'Docs updated', storedCompleted: true, effectiveCompleted: true,
483
477
  linkedTicketId: null, linkedTicketKey: null, linkedTicketTitle: null, linkedTicketStatusCategory: null, sortOrder: 0 },
484
478
  { id: 'i2', content: 'Sub-task done', storedCompleted: false, effectiveCompleted: false,
@@ -517,11 +511,10 @@ describe('orboto_get_timer', () => {
517
511
  expect(res.structuredContent).toEqual({ timer: null });
518
512
  });
519
513
  it('computes totalSeconds including elapsed time since startedAt', async () => {
520
- const startedAt = new Date(Date.now() - 120_000).toISOString(); // 2 min ago
514
+ const startedAt = new Date(Date.now() - 120_000).toISOString();
521
515
  stub([{ json: { id: 'tm1', userId: 'u1', ticketId: 't1', ticketTitle: 'Bug', startedAt, pausedAt: null, accumulatedSeconds: 300, description: 'debug' } }]);
522
516
  const res = await makeGetTimerHandler(client)();
523
517
  const sc = res.structuredContent;
524
- // 300 accumulated + ~120 elapsed. Allow ±5s for test timing jitter.
525
518
  expect(sc.timer.totalSeconds).toBeGreaterThanOrEqual(418);
526
519
  expect(sc.timer.totalSeconds).toBeLessThanOrEqual(425);
527
520
  expect(sc.timer.paused).toBe(false);
@@ -1,33 +1,12 @@
1
1
  /**
2
2
  * ORB-510 / ORB-513 - primer-fact tools.
3
3
  *
4
- * Six tools that wrap the `/projects/:id/primer-facts` and
5
- * `/primer-facts/:id` REST surface from ORB-511. Agents call these to
6
- * record structured project facts - tech-stack details, conventions,
7
- * deployment quirks - that the AI primer (ORB-512) renders at the top
8
- * of every session. The skill rule (ORB-514) tells agents *when* to
9
- * record; these tools are the *how*.
10
- *
11
- * All tool descriptions and parameter `.describe()` strings are in
12
- * English so the international LLM tool-selection path is reliable.
13
- *
14
- * Wire order in server.ts:
15
- * - orboto_primer_fact_list
16
- * - orboto_primer_fact_add
17
- * - orboto_primer_fact_update
18
- * - orboto_primer_fact_supersede
19
- * - orboto_primer_fact_verify
20
- * - orboto_primer_fact_delete
4
+ * @see ORB-511, ORB-512, ORB-514
21
5
  */
22
6
  import { z } from 'zod';
23
7
  import { OrbotoApiError } from '../orboto-client.js';
24
8
  import { resolveProjectByKey, sizeBlockResult } from './shared.js';
25
- // ORB-1819 - the writing contract, verbatim in every write-tool
26
- // description so the writing agent sees it at the moment of writing.
27
9
  const FACT_WRITING_CONTRACT = ' Fact: <= 300 chars, one fact per key, the value is the fact - long material goes into a doc, the fact holds the doc key.';
28
- // ---------------------------------------------------------------------------
29
- // Shared types + helpers
30
- // ---------------------------------------------------------------------------
31
10
  const PRIMER_FACT_CATEGORIES = [
32
11
  'tech_stack',
33
12
  'conventions',
@@ -36,11 +15,7 @@ const PRIMER_FACT_CATEGORIES = [
36
15
  'integrations',
37
16
  'gotchas',
38
17
  'commands',
39
- // ORB-1403 - explicit non-functional requirements (performance, security,
40
- // DSGVO, operations, i18n, ...) for the Pflichtenheft NFR chapter.
41
18
  'non_functional',
42
- // ORB-1413 - glossary/terminology facts for the Pflichtenheft glossary
43
- // chapter (chapter 11) only, so they no longer double-render via `other`.
44
19
  'glossary',
45
20
  'other',
46
21
  ];
@@ -70,17 +45,12 @@ function summariseFact(f) {
70
45
  if (f.supersededById)
71
46
  markers.push('superseded');
72
47
  const tag = markers.length > 0 ? ` _(${markers.join(', ')})_` : '';
73
- // Single-line preview of the value - multi-line bodies get truncated
74
- // so the list output stays scannable.
75
48
  const valuePreview = f.value.length > 120 || f.value.includes('\n')
76
49
  ? `${f.value.replace(/\n/g, ' ').slice(0, 120)}…`
77
50
  : f.value;
78
51
  const scope = f.projectId === null ? '[workspace]' : '[project]';
79
52
  return `- ${scope} **${f.category}/${f.key}**: ${valuePreview}${tag}`;
80
53
  }
81
- // ---------------------------------------------------------------------------
82
- // orboto_primer_fact_list
83
- // ---------------------------------------------------------------------------
84
54
  export const primerFactListToolConfig = {
85
55
  title: 'List structured project primer facts',
86
56
  description: 'List structured project facts that feed the AI primer (tech stack, conventions, deployment, architecture, integrations, gotchas, commands). Use this to discover what the project has already documented before adding a new observation. Workspace-wide facts (applying to every project) are merged in by default; pass includeWorkspace=false to see only project-scoped rows.',
@@ -127,9 +97,6 @@ export function makePrimerFactListHandler(client) {
127
97
  };
128
98
  };
129
99
  }
130
- // ---------------------------------------------------------------------------
131
- // orboto_primer_fact_add
132
- // ---------------------------------------------------------------------------
133
100
  export const primerFactAddToolConfig = {
134
101
  title: 'Record a new project primer fact',
135
102
  description: 'Record a new structured fact about the project that future AI agents should see at session start. Use when you learn something during work that is missing from the primer (a library version, a convention, a deployment quirk, an architecture decision). Note: the auto-generated primer does NOT automatically include CLAUDE.md / AGENTS.md content unless the operator configured repo briefings and the API host can read those files from disk - most deployments cannot. Treat key conventions documented in CLAUDE.md / AGENTS.md as fair game to record here so they survive cross-deployment. Set observed=true when you are recording from a bot/agent context - the fact will land as agent_observed and require operator verification before losing the (observed) marker.'
@@ -182,9 +149,6 @@ export function makePrimerFactAddHandler(client) {
182
149
  };
183
150
  };
184
151
  }
185
- // ---------------------------------------------------------------------------
186
- // orboto_primer_fact_update
187
- // ---------------------------------------------------------------------------
188
152
  export const primerFactUpdateToolConfig = {
189
153
  title: 'Update an existing primer fact',
190
154
  description: 'Update an existing fact. Use this when the value has changed (e.g. version bumped) or you are renaming the key. Bumps last_verified_at automatically when the value changes; pure renames leave it untouched. To replace a fact while preserving history, use orboto_primer_fact_supersede instead.'
@@ -240,9 +204,6 @@ export function makePrimerFactUpdateHandler(client) {
240
204
  };
241
205
  };
242
206
  }
243
- // ---------------------------------------------------------------------------
244
- // orboto_primer_fact_supersede
245
- // ---------------------------------------------------------------------------
246
207
  export const primerFactSupersedeToolConfig = {
247
208
  title: 'Replace a primer fact while preserving history',
248
209
  description: 'Replace a fact with a corrected version, preserving the old one in history (linked via superseded_by_id). Use when you discover a previously recorded fact was wrong; do not just delete it. The old row gets a tombstoned key suffix so the partial unique index does not collide; the new row starts unverified and needs operator verification before losing the (observed) marker.'
@@ -284,9 +245,6 @@ export function makePrimerFactSupersedeHandler(client) {
284
245
  };
285
246
  };
286
247
  }
287
- // ---------------------------------------------------------------------------
288
- // orboto_primer_fact_verify
289
- // ---------------------------------------------------------------------------
290
248
  export const primerFactVerifyToolConfig = {
291
249
  title: 'Verify an agent-observed primer fact',
292
250
  description: 'Promote an agent-observed fact to verified. Operator-only action; bots without project:edit cannot self-verify. Removes the (observed) marker from the rendered primer and stamps verifiedBy / verifiedAt.',
@@ -314,9 +272,6 @@ export function makePrimerFactVerifyHandler(client) {
314
272
  };
315
273
  };
316
274
  }
317
- // ---------------------------------------------------------------------------
318
- // orboto_primer_fact_delete
319
- // ---------------------------------------------------------------------------
320
275
  export const primerFactDeleteToolConfig = {
321
276
  title: 'Delete a primer fact',
322
277
  description: 'Hard-delete a fact. Prefer orboto_primer_fact_supersede when the fact is being replaced; only delete when the fact was wrong from the start or no longer relevant. Writes an audit-log entry; the row itself is gone (no soft-delete).',
@@ -328,9 +283,6 @@ export const primerFactDeleteToolConfig = {
328
283
  };
329
284
  export function makePrimerFactDeleteHandler(client) {
330
285
  return async ({ factId, reason }) => {
331
- // ORB-516 - pass the reason through so the audit-log entry
332
- // captures it. Old API versions silently ignore the querystring
333
- // so there's no compat risk.
334
286
  const path = reason
335
287
  ? `/primer-facts/${factId}?reason=${encodeURIComponent(reason)}`
336
288
  : `/primer-facts/${factId}`;
@@ -50,9 +50,6 @@ const factStub = {
50
50
  createdAt: '2026-04-30T10:00:00Z',
51
51
  updatedAt: '2026-04-30T10:00:00Z',
52
52
  };
53
- // ---------------------------------------------------------------------------
54
- // orboto_primer_fact_list
55
- // ---------------------------------------------------------------------------
56
53
  describe('orboto_primer_fact_list', () => {
57
54
  it('resolves projectKey then GETs /projects/<id>/primer-facts with filters', async () => {
58
55
  const calls = stub([
@@ -118,9 +115,6 @@ describe('orboto_primer_fact_list', () => {
118
115
  expect(result.success).toBe(false);
119
116
  });
120
117
  });
121
- // ---------------------------------------------------------------------------
122
- // orboto_primer_fact_add
123
- // ---------------------------------------------------------------------------
124
118
  describe('orboto_primer_fact_add', () => {
125
119
  it('observed=false (default) sends source=manual', async () => {
126
120
  const calls = stub([
@@ -189,9 +183,6 @@ describe('orboto_primer_fact_add', () => {
189
183
  expect(result.success).toBe(false);
190
184
  });
191
185
  });
192
- // ---------------------------------------------------------------------------
193
- // orboto_primer_fact_update
194
- // ---------------------------------------------------------------------------
195
186
  describe('orboto_primer_fact_update', () => {
196
187
  it('PATCHes /primer-facts/<id> with only the provided fields', async () => {
197
188
  const calls = stub([{ json: { ...factStub, value: 'pnpm 10' } }]);
@@ -211,9 +202,6 @@ describe('orboto_primer_fact_update', () => {
211
202
  await expect(makePrimerFactUpdateHandler(client)({ factId: 'missing', value: 'v' })).rejects.toThrow(/not found/);
212
203
  });
213
204
  });
214
- // ---------------------------------------------------------------------------
215
- // orboto_primer_fact_supersede
216
- // ---------------------------------------------------------------------------
217
205
  describe('orboto_primer_fact_supersede', () => {
218
206
  it('POSTs /primer-facts/<id>/supersede with category + key + value', async () => {
219
207
  const calls = stub([{ json: { ...factStub, id: 'new-uuid', value: 'pnpm 10' } }]);
@@ -237,9 +225,6 @@ describe('orboto_primer_fact_supersede', () => {
237
225
  expect(schema.safeParse({ oldFactId: 'x' }).success).toBe(false);
238
226
  });
239
227
  });
240
- // ---------------------------------------------------------------------------
241
- // orboto_primer_fact_verify
242
- // ---------------------------------------------------------------------------
243
228
  describe('orboto_primer_fact_verify', () => {
244
229
  it('POSTs /primer-facts/<id>/verify with empty body', async () => {
245
230
  const calls = stub([{ json: { ...factStub, verified: true, verifiedBy: 'u1', verifiedAt: 'now' } }]);
@@ -249,9 +234,6 @@ describe('orboto_primer_fact_verify', () => {
249
234
  expect(calls[0].body).toEqual({});
250
235
  });
251
236
  });
252
- // ---------------------------------------------------------------------------
253
- // orboto_primer_fact_delete
254
- // ---------------------------------------------------------------------------
255
237
  describe('orboto_primer_fact_delete', () => {
256
238
  it('DELETEs /primer-facts/<id>', async () => {
257
239
  const calls = stub([{ status: 204 }]);
@@ -275,8 +257,6 @@ describe('orboto_primer_fact_delete', () => {
275
257
  await expect(makePrimerFactDeleteHandler(client)({ factId: 'missing' })).rejects.toThrow(/not found/);
276
258
  });
277
259
  });
278
- // ORB-1819 - the writing-for-tokens size contract, passed through from the
279
- // REST route's warn (200/201 + sizeWarning) / block (422) responses.
280
260
  describe('ORB-1819 size contract', () => {
281
261
  it('add surfaces a soft-limit sizeWarning in the success text, not as an error', async () => {
282
262
  stub([
@@ -1,26 +1,8 @@
1
1
  /**
2
2
  * ORB-799 - per-project metadata listings.
3
- *
4
- * Two cheap read tools that round-trip the project-scoped enum tables
5
- * agents repeatedly need:
6
- *
7
- * - orboto_list_ticket_statuses - the workflow's status rows with
8
- * their IDs, categories, colors, and terminal flag. Needed for
9
- * typed-transition flows where the standard `move_ticket` (which
10
- * picks the first status per category) isn't precise enough.
11
- *
12
- * - orboto_list_labels - the per-project label catalogue with
13
- * colors. Lets agents see what labels exist before calling
14
- * `create_ticket` with a `labels` array (unknown names error).
15
- *
16
- * Both mirror the wrapper's `statuses <project>` / `labels <project>`
17
- * subcommands.
18
3
  */
19
4
  import { z } from 'zod';
20
5
  import { resolveProjectByKey } from './shared.js';
21
- // ---------------------------------------------------------------------------
22
- // orboto_list_ticket_statuses
23
- // ---------------------------------------------------------------------------
24
6
  export const listTicketStatusesToolConfig = {
25
7
  title: 'List a project\'s ticket statuses',
26
8
  description: 'Return the project\'s workflow rows - `{id, name, category, color, sortOrder, isTerminal}`. The `category` is the broad bucket (todo / in_progress / in_review / done / wont_fix); `name` is the workspace\'s display label (e.g. "Code Review"). Use the `id` to do typed transitions through `orboto_update_ticket` when the standard category-based move is too coarse.',
@@ -52,9 +34,6 @@ export function makeListTicketStatusesHandler(client) {
52
34
  };
53
35
  };
54
36
  }
55
- // ---------------------------------------------------------------------------
56
- // orboto_list_labels
57
- // ---------------------------------------------------------------------------
58
37
  export const listLabelsToolConfig = {
59
38
  title: 'List a project\'s labels',
60
39
  description: 'Return the project\'s label catalogue - `{id, name, color}`. Labels must be referenced by exact name when assigning via `orboto_create_ticket` (the API errors on unknown names rather than auto-creating); use this tool to discover what\'s available before assigning.',
@@ -79,9 +58,6 @@ export function makeListLabelsHandler(client) {
79
58
  };
80
59
  };
81
60
  }
82
- // ---------------------------------------------------------------------------
83
- // orboto_create_label - ORB-1041
84
- // ---------------------------------------------------------------------------
85
61
  export const createLabelToolConfig = {
86
62
  title: 'Create a project label',
87
63
  description: 'Create a label in a project so it can be assigned to tickets. Closes the gap where `orboto_create_ticket` errors on an unknown label name (labels are not auto-created). Idempotent: if a label with the same name already exists, it is returned instead of erroring. Needs project:edit.',
@@ -95,7 +71,6 @@ export const createLabelToolConfig = {
95
71
  export function makeCreateLabelHandler(client) {
96
72
  return async ({ projectKey, name, color }) => {
97
73
  const project = await resolveProjectByKey(client, projectKey);
98
- // Idempotent: return an existing same-name label rather than erroring.
99
74
  const existing = await client.get(`/projects/${project.id}/labels`);
100
75
  const match = existing.find((l) => l.name.toLowerCase() === name.toLowerCase());
101
76
  if (match) {
@@ -1,17 +1,7 @@
1
1
  /**
2
2
  * ORB-273 Phase F - `orboto_query`.
3
3
  *
4
- * Lets the model reach for the OQL DSL when the per-entity list
5
- * tools (`orboto_list_tickets`, `orboto_my_tickets`) get too narrow:
6
- * combined `assignee + dueDate + label + statusCategory` filters
7
- * with explicit ORDER BY are awkward to express through tool
8
- * arguments but trivial in OQL. Falls back to JQL syntax via
9
- * `syntax: 'jql'` for migrants who copy-pasted a Jira query.
10
- *
11
- * The endpoint is `POST /query` (ORB-531) which honours PBAC,
12
- * cursor-paginates, and rate-limits at 60/min. We expose the
13
- * underlying envelope unchanged so a future "give me the next page"
14
- * tool can plumb the cursor through.
4
+ * @see ORB-531
15
5
  */
16
6
  import { z } from 'zod';
17
7
  import { ticketLine, agentTicketListRow } from './shared.js';
@@ -57,7 +47,6 @@ export function makeQueryHandler(client) {
57
47
  structuredContent: {
58
48
  count: page.items.length,
59
49
  nextCursor: page.nextCursor,
60
- // ORB-1699 - shared lean row; verbose restores uuid/labels/minutes.
61
50
  tickets: page.items.map((t) => agentTicketListRow(t, input.verbose ?? false)),
62
51
  },
63
52
  };
@@ -47,8 +47,6 @@ describe('tools/query', () => {
47
47
  body: JSON.stringify({ oql: 'project = ORB', syntax: 'oql', cursor: undefined, limit: 25 }),
48
48
  }));
49
49
  expect(result.content[0]).toMatchObject({ type: 'text', text: expect.stringContaining('ORB-1') });
50
- // ORB-1699 - the default row is the shared lean projection: no uuid,
51
- // no labels, no minutes; assignee NAMES (fullName || email).
52
50
  expect(result.structuredContent).toMatchObject({
53
51
  count: 1,
54
52
  nextCursor: null,
@@ -88,10 +86,6 @@ describe('tools/query', () => {
88
86
  }));
89
87
  });
90
88
  });
91
- // ORB-1699 - the ONE row builder, asserted from one place: the lean shape
92
- // omits uuid/timestamps/minutes; verbose restores them. list_tickets,
93
- // my_tickets and query all consume agentTicketListRow, so this single
94
- // test pins the row for all three.
95
89
  import { agentTicketListRow } from './shared.js';
96
90
  describe('agentTicketListRow (ORB-1699)', () => {
97
91
  const FULL = {
@@ -104,8 +98,6 @@ describe('agentTicketListRow (ORB-1699)', () => {
104
98
  };
105
99
  it('lean row: decision fields only, defaults omitted', () => {
106
100
  const row = agentTicketListRow(FULL);
107
- // priority 'high' and dueDate present -> included; type stays only
108
- // when it deviates from 'task'; status NAME is verbose-only.
109
101
  expect(Object.keys(row).sort()).toEqual(['assigneeNames', 'dueDate', 'key', 'priority', 'statusCategory', 'title']);
110
102
  expect(row.assigneeNames).toEqual(['Ada']);
111
103
  });
@@ -17,9 +17,6 @@ async function resolveMemberId(client, projectId, email) {
17
17
  throw new Error(`No project member with email "${email}".`);
18
18
  return m.userId;
19
19
  }
20
- // ---------------------------------------------------------------------------
21
- // orboto_raci - read the matrix
22
- // ---------------------------------------------------------------------------
23
20
  export const raciToolConfig = {
24
21
  title: 'RACI matrix',
25
22
  description: "Read a project's RACI matrix (tickets x members, cells = R/A/C/I). `milestone` scopes to one milestone; `epicsOnly` narrows rows to epics. RACI is OPT-IN per project: only use this when the project has RACI enabled (check `raciEnabled` on orboto_get_project / the primer). Do NOT raise or suggest RACI on projects that haven't enabled it. Returns empty when RACI is off.",
@@ -35,8 +32,6 @@ export function makeRaciHandler(client) {
35
32
  const project = await resolveProjectByKey(client, input.projectKey);
36
33
  const qs = new URLSearchParams();
37
34
  if (input.milestone) {
38
- // ORB-1696 - shared resolver: key (ORB-M3), name or UUID, ambiguous
39
- // name -> explicit error. Matches create_ticket/set_milestone/OQL.
40
35
  const m = await resolveMilestoneByNameOrId(client, project.id, input.milestone);
41
36
  qs.set('milestoneId', m.id);
42
37
  }
@@ -49,7 +44,6 @@ export function makeRaciHandler(client) {
49
44
  structuredContent: { raciEnabled: false, projectKey: project.key, members: [], rows: [] },
50
45
  };
51
46
  }
52
- // Compact text rendering: one line per ticket listing each held role.
53
47
  const lines = data.rows.map((r) => {
54
48
  const byUser = new Map(data.members.map((m) => [m.userId, m.fullName]));
55
49
  const held = Object.entries(r.cells)
@@ -64,9 +58,6 @@ export function makeRaciHandler(client) {
64
58
  };
65
59
  };
66
60
  }
67
- // ---------------------------------------------------------------------------
68
- // orboto_set_raci - write a role
69
- // ---------------------------------------------------------------------------
70
61
  export const setRaciToolConfig = {
71
62
  title: 'Set a RACI role',
72
63
  description: "Set a person's RACI role on a ticket: R (Responsible), A (Accountable, max one per ticket), C (Consulted), or I (Informed). RACI is OPT-IN per project: only use this when the project has RACI enabled (`raciEnabled` true) - do NOT suggest or set RACI on projects that haven't opted in. Resolves the user by email within the ticket's project. Requires ticket:manage_raci. A second Accountable is rejected with the current holder's name.",
@@ -86,8 +77,6 @@ export function makeSetRaciHandler(client) {
86
77
  }
87
78
  catch (err) {
88
79
  if (err instanceof OrbotoApiError && err.status === 409) {
89
- // Single-A violation or RACI-not-enabled - surface the clean message
90
- // the API put in the response body.
91
80
  let msg = err.body;
92
81
  try {
93
82
  msg = JSON.parse(err.body).error ?? err.body;
@@ -1,18 +1,7 @@
1
1
  /**
2
2
  * ORB-1409 (epic ORB-1390) - `orboto_requirements_spec`.
3
3
  *
4
- * Generates the requirements specification (Pflichtenheft) as Markdown:
5
- * numbered functional requirements (FA-1, FA-1.1...) each traceable to its
6
- * ticket and tagged muss/soll/kann from priority, plus non-functional
7
- * requirements distilled from primer facts. `outlineVariant` picks the chapter
8
- * naming/order: `neutral` (default), `industry` (VDI-3694 style), or `software`
9
- * (IEEE-830 style). Private tickets/milestones are always excluded server-side.
10
- * Wraps `POST /projects/:id/requirements-spec/generate` with `format: 'markdown'`.
11
- * Money price mode needs budget:view on top of requirements_spec:generate (the
12
- * API returns 403 otherwise).
13
- *
14
- * The route existed since ORB-1391; ORB-1409 fills in the missing MCP + skill
15
- * surfaces (the Pflichtenheft feature shipped route + chat only).
4
+ * @see ORB-1391
16
5
  */
17
6
  import { z } from 'zod';
18
7
  import { OrbotoApiError } from '../orboto-client.js';
@@ -1,20 +1,6 @@
1
1
  /**
2
2
  * ORB-1697 - `orboto_response_expand`: the way back from a truncated
3
3
  * response.
4
- *
5
- * The central response budget (`../response-budget.ts`) caps what a
6
- * single tool result may inject into the session, because a result is
7
- * re-sent on every later request and therefore costs its size times the
8
- * turns that follow it. A cap is only acceptable if nothing is lost, so
9
- * every truncated response carries a handle, and this tool serves the
10
- * omitted remainder from the in-process store in budget-sized chunks.
11
- *
12
- * Deliberately NOT a REST call: the handle addresses the payload the
13
- * ORIGINAL call already produced, so expanding cannot return anything
14
- * the caller was not already authorised to see, and it costs no
15
- * additional API round-trip. The store is per-process and short-lived
16
- * (15 min, 16 payloads) - an expired handle is answered with "re-run the
17
- * original tool", never with a wrong or stale chunk.
18
4
  */
19
5
  import { z } from 'zod';
20
6
  import { budgetFor, readPayload, resolvePath, } from '../response-budget.js';
@@ -48,8 +34,6 @@ export function makeResponseExpandHandler() {
48
34
  if (!stored)
49
35
  return expired(input.handle);
50
36
  const omitted = stored.omitted ?? [];
51
- // No path - list what can be expanded. Cheap by construction: paths
52
- // and counts only, never the content itself.
53
37
  if (!input.path) {
54
38
  const lines = [
55
39
  `# Truncated content available for handle "${input.handle}"`,
@@ -77,9 +61,6 @@ export function makeResponseExpandHandler() {
77
61
  return { isError: true, content: [{ type: 'text', text }] };
78
62
  }
79
63
  const full = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 1) ?? '';
80
- // Leave room for the framing lines so a chunk plus its header still
81
- // fits this tool's own budget - a continuation tool that gets
82
- // truncated itself would be a loop, not a fix.
83
64
  const chunkSize = Math.max(500, budgetFor('orboto_response_expand') - 800);
84
65
  const cursor = Math.min(input.cursor ?? 0, full.length);
85
66
  const chunk = full.slice(cursor, cursor + chunkSize);
@@ -1,26 +1,8 @@
1
1
  /**
2
2
  * ORB-1615 - structural review policy tools.
3
- *
4
- * - orboto_review_fingerprint - hash exact raw diff text into a
5
- * canonical fingerprint (+ real size
6
- * metrics), server-side (one algorithm,
7
- * not one per client).
8
- * - orboto_review_policy_check - the consult-before-invoking-a-model
9
- * call: what risk level does this ticket carry, and (if a fingerprint
10
- * is supplied) is there already a valid approval for THIS exact diff.
11
- * - orboto_review_approval_record - record an approve/reject decision
12
- * against a ticket's diff fingerprint.
13
- *
14
- * Rule CONFIGURATION (path pattern / deliveryMode / size -> risk level)
15
- * stays out of the MCP surface, same call as approval_policies' CRUD in
16
- * tools/approvals.ts: it's project-settings admin config, not something an
17
- * autonomous agent tool should expose.
18
3
  */
19
4
  import { z } from 'zod';
20
5
  import { resolveTicketByKey } from './shared.js';
21
- // ---------------------------------------------------------------------------
22
- // orboto_review_fingerprint
23
- // ---------------------------------------------------------------------------
24
6
  export const reviewFingerprintToolConfig = {
25
7
  title: 'Compute a canonical diff fingerprint',
26
8
  description: 'Hash the exact submitted diff text server-side with sha256-diff-v2. Every text change, including indentation, blank lines, binary blob identities, mode/rename metadata and file order, changes the fingerprint. This is submitted-text identity, NOT independent repository attestation. Returns size/path metrics too. Preserve the complete opaque version-prefixed `fingerprint` for `orboto_review_policy_check` and `orboto_review_approval_record`; legacy v1 approvals remain history only and must be reviewed again.',
@@ -41,9 +23,6 @@ export function makeReviewFingerprintHandler(client) {
41
23
  };
42
24
  };
43
25
  }
44
- // ---------------------------------------------------------------------------
45
- // orboto_review_policy_check
46
- // ---------------------------------------------------------------------------
47
26
  export const reviewPolicyCheckToolConfig = {
48
27
  title: 'Check a ticket\'s review policy before invoking a reviewer',
49
28
  description: 'Consult a ticket\'s structural review policy BEFORE spawning a review session or invoking a model. Called with no diff context (the common case, right after picking up a ticket) it resolves the per-ticket override or a deliveryMode-only rule; passing `paths`/`linesChanged` (from `orboto_review_fingerprint`) additionally matches path/size-scoped rules and, if `fingerprint` is passed too, checks whether a VALID approval already covers this exact diff - so a low-risk ticket, or one already reviewed at this fingerprint, can close without spawning a reviewer. `riskLevel: "required"` with `source: "fail_safe"` means the policy engine itself errored - treat that as "review required", never as "no review needed".',
@@ -71,9 +50,6 @@ export function makeReviewPolicyCheckHandler(client) {
71
50
  };
72
51
  };
73
52
  }
74
- // ---------------------------------------------------------------------------
75
- // orboto_review_approval_record
76
- // ---------------------------------------------------------------------------
77
53
  export const reviewApprovalRecordToolConfig = {
78
54
  title: 'Record a review decision against a diff fingerprint',
79
55
  description: 'Record your review verdict (approve/reject) against the complete version-prefixed fingerprint from orboto_review_fingerprint. A current-algorithm approval is reusable only for identical submitted diff text; even whitespace or file-order changes require a fresh review. Legacy/unversioned records remain history only. This advisory review ledger does not itself enforce ticket closure. Requires ticket:record_review_approval.',
@@ -66,8 +66,6 @@ describe('orboto_review_approval_record', () => {
66
66
  const res = await makeReviewApprovalRecordHandler(client)({ ticketKey: 'ACME-1', fingerprint: FINGERPRINT, decision: 'approved', note: 'looks good' });
67
67
  const record = calls.find((c) => c.method === 'POST' && c.url.includes('review-approvals'));
68
68
  expect(record?.body).toMatchObject({ fingerprint: FINGERPRINT, decision: 'approved', note: 'looks good' });
69
- // ticketKey must NOT leak into the request body sent to the API - the
70
- // route resolves the ticket from the URL, not the payload.
71
69
  expect(record?.body).not.toHaveProperty('ticketKey');
72
70
  expect(res.content[0].text).toContain('approved');
73
71
  });
@@ -1,17 +1,7 @@
1
1
  /**
2
2
  * ORB-244 Phase B - `orboto_search`.
3
3
  *
4
- * Unified full-text search across tickets, comments, and docs. Maps
5
- * to the existing `/search` route (Phase 21 global search) which
6
- * already respects PBAC visibility (private tickets, internal
7
- * comments, doc ACL).
8
- *
9
- * ORB-272: /search is cursor-paginated. Response shape is
10
- * `{items, nextCursor, total}`. We surface `total` so the model
11
- * knows how many hits exist globally, but don't expose `cursor` on
12
- * the tool input - repeat MCP tool calls to walk a search cursor is
13
- * an awkward UX. Users who need to see more narrow the query or
14
- * open the full-page /search in the web UI.
4
+ * @see ORB-272
15
5
  */
16
6
  import { z } from 'zod';
17
7
  import { resolveProjectByKey } from './shared.js';
@@ -49,8 +39,6 @@ export function makeSearchHandler(client) {
49
39
  ? `No hits for "${input.query}".`
50
40
  : `Hits${headerHint}${relaxedNote}:\n\n` + res.items.map((h) => {
51
41
  const tag = h.type.toUpperCase();
52
- // ORB-1084 - non-ticket hits carry the FULL id: the truncated
53
- // form was unusable as input for the doc write tools.
54
42
  const ident = h.ticketKey ?? h.id;
55
43
  const project = h.projectName ? ` · ${h.projectName}` : '';
56
44
  return `- [${tag} ${ident}${project}] ${h.title}\n ${h.excerpt}`;