@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.
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
@@ -6,46 +6,7 @@
6
6
  * briefing so the agent re-anchors on how to work and what it was
7
7
  * doing. Read-only.
8
8
  *
9
- * ORB-1605 - also surfaces git-connection health for the projects the
10
- * caller currently has open work in. A dead/unhealthy connection means
11
- * commit ingestion may be stalled, which the agent should know about
12
- * BEFORE it assumes a closing check that depends on git activity is
13
- * reliable.
14
- *
15
- * ORB-1607 - lean startup contract:
16
- * (a) rules-hash ack. `/agent-instructions` now returns a stable
17
- * `rulesHash`. This handler remembers the last hash it saw FOR
18
- * THE LIFETIME OF THIS MCP CONNECTION (one handler closure per
19
- * `buildOrbotoMcpServer` call - see server.ts) and passes it back
20
- * as `knownRulesHash` on every subsequent call. An unchanged hash
21
- * collapses the multi-thousand-token rules block into a one-line
22
- * ack, which is most of the field-measured 12-18k token/session
23
- * cost on a workspace with configured instruction blocks. Never
24
- * exposed as a tool input - the caller (an LLM) just calls the
25
- * tool the same way every time; the cache is transparent.
26
- * (b) optional `ticketKey` input bundles a project primer, the full
27
- * ticket (incl. dependencies + checklists), that project's git
28
- * health, and any other agent sessions currently working the same
29
- * ticket into the SAME response - replacing what would otherwise
30
- * be >=4 separate tool calls (get_project_primer, get_ticket,
31
- * get_checklists, list_ticket_dependencies) at the point an agent
32
- * has the least context loaded.
33
- *
34
- * ORB-1818 - the rules INDEX. Measured on production 2026-09-03: the
35
- * last 15 `session_start` calls cost 21.8k-29.3k characters each, 89 %
36
- * of it the assembled rule text, and none was ever truncated (the tool
37
- * held a 48k per-tool budget). A result is re-sent on every later
38
- * request, so that block was the single largest carry cost an agent
39
- * paid, on every cold start, whether or not it needed a single rule.
40
- *
41
- * The default answer now carries the rules HASH plus one line per rule
42
- * block (title only, in delivery order) and a `response_expand` handle;
43
- * the full, byte-identical text is one call away via `rulesOnly: true`
44
- * (stateless, never truncated) or that handle (in-process, 15 min). The
45
- * ack semantics are unchanged: a caller that already acked this exact
46
- * hash gets neither the text NOR the index, and `forceRules: true`
47
- * still returns the full text inline for offline agents and operator
48
- * debugging.
9
+ * @see ORB-1605, ORB-1607, ORB-1818
49
10
  */
50
11
  import { z } from 'zod';
51
12
  import { resolveTicketByKey, applyAgentProfile } from './shared.js';
@@ -55,29 +16,17 @@ import { GIT_HEALTH_REASON_TEXT } from './git-health-reasons.js';
55
16
  export const sessionStartToolConfig = {
56
17
  title: 'Load the rules you must follow + re-orient',
57
18
  description: 'THE canonical way to LOAD the binding workspace rules you must follow as an agent. Run it as your FIRST action in a session and immediately AFTER any context compaction. Returns a one-line-per-rule INDEX of the binding working-rules plus their hash (or a compact "unchanged" ack on a repeat call within the same connection) - read the index, then call this tool again with `rulesOnly: true` to read the full text of the rules whenever you do not already hold that exact hash, and expand before acting on any rule whose title touches what you are about to do. Also returns your in-progress tickets - each flagged LANDED, IDLE when it has a linked commit but has not moved for days, i.e. finished work you never handed to review - your running timer, and a warning if a project\'s git connection looks unhealthy (commit ingestion may be stalled). Pass `ticketKey` to also get a one-shot bundle for that ticket: project primer, the full ticket with dependencies + checklists, that project\'s git health, and any other agent sessions currently on it - replacing several separate calls. (Do NOT use orboto_list_agent_instructions to read the rules - that tool MANAGES/edits rule blocks for admins; this one is what you read to know how to work.) Read-only; no side effects. '
58
- // ORB-1805 - parameter prose moved out of the input schema (paid for
59
- // at every connect) into the description orboto_help serves in full.
60
19
  + '**Parameters.** `rulesOnly: true` returns ONLY the complete rules text (nothing else) and is never truncated - the cheapest way to read the rules the index listed. `projectId` adds that project\'s rules on top of the workspace + personal ones. `ticketKey` ("ACME-42") bundles that ticket\'s primer, full detail, dependencies, checklists, git health and other active agent sessions into the same response. `forceRules: true` returns the full rules text inline with the rest of the digest even when this connection already delivered them - use it whenever the rules are NOT in your context right now: after a compaction, a /clear, or a fresh agent taking over an existing connection. `agentKind` (coding, orchestrator, reviewer, runner) and `modelTier` (frontier, standard, small) are your self-declared classification; rule blocks and per-tier rule text are targeted by them.',
61
20
  inputSchema: z.object({
62
21
  projectId: z.string().uuid().optional().describe('Also load this project\'s rules.'),
63
22
  ticketKey: z.string().min(3).optional().describe('Ticket key ("ACME-42") - bundles that ticket\'s full context.'),
64
- // ORB-1697 - the caller is the only party that knows whether it still
65
- // HOLDS the rules. See the ack-defect note on the handler below.
66
23
  forceRules: z.boolean().optional().describe('Always return the full rules; set it when you do not hold them.'),
67
- // ORB-1818 - the stateless way back from the index to the full text.
68
24
  rulesOnly: z.boolean().optional().describe('Return ONLY the complete rules text.'),
69
- // ORB-1753 - self-declared classification for rule targeting; defaults
70
- // to ORBOTO_AGENT_KIND / ORBOTO_MODEL_TIER env, then the api-key's
71
- // standing profile server-side.
72
25
  agentKind: z.string().min(1).max(32).optional().describe('coding | orchestrator | reviewer | runner.'),
73
26
  modelTier: z.string().min(1).max(32).optional().describe('frontier | standard | small.'),
74
27
  }).shape,
75
28
  annotations: { readOnlyHint: true, idempotentHint: true },
76
29
  };
77
- // Cap how many distinct projects we probe for git health - a session's
78
- // in-progress work is capped at 20 tickets already, so this rarely
79
- // exceeds a handful, but bound it defensively so a pathological account
80
- // can't turn session-start into N parallel requests.
81
30
  const MAX_GIT_HEALTH_PROJECTS = 8;
82
31
  /** ORB-1607 - build the `--ticket` one-shot bundle. Never throws: an
83
32
  * unresolvable/unauthorized ticket key comes back as a text error
@@ -96,9 +45,6 @@ async function buildTicketBundle(client, ticketKey) {
96
45
  };
97
46
  }
98
47
  const [enriched, checklists, deps, primer, gitHealthConnections, activeSessions] = await Promise.all([
99
- // resolveTicketByKey hits the by-key endpoint, which returns a bare
100
- // row (no statusCategory / assignees / description). Re-fetch the
101
- // enriched by-id shape, mirroring orboto_get_ticket.
102
48
  client.get(`/projects/${resolved.projectId}/tickets/${resolved.id}`).catch(() => resolved),
103
49
  client.get(`/tickets/${resolved.id}/checklists`)
104
50
  .then((r) => (Array.isArray(r) ? r : []))
@@ -115,19 +61,9 @@ async function buildTicketBundle(client, ticketKey) {
115
61
  .get(`/projects/${resolved.projectId}/git-health`)
116
62
  .then((r) => (Array.isArray(r?.connections) ? r.connections : []))
117
63
  .catch(() => []),
118
- // ORB-704 - non-admins only see their own sessions; that's an
119
- // acceptable "cheaply available" degrade, not a bug to work around.
120
64
  client.get('/v1/agent/presence').catch(() => []),
121
65
  ]);
122
- // ORB-1697 - `enriched ?? resolved` trusted truthiness: a 200 whose body
123
- // is `{}` is truthy, so the fallback never fired and the bundle rendered
124
- // "Ticket bundle: undefined" with empty fields. Fall back on the row we
125
- // already resolved unless the enriched read actually carries a ticket.
126
66
  const full = enriched?.ticketKey ? enriched : resolved;
127
- // ORB-1697 - the `.catch` above only covers a REJECTED request. A 200 with
128
- // an unexpected body (a wrapped/paginated shape, or an older instance)
129
- // would make `.filter` throw and take the whole digest down over an
130
- // optional section. Coerce instead.
131
67
  const sessions = Array.isArray(activeSessions) ? activeSessions : [];
132
68
  const connections = Array.isArray(gitHealthConnections) ? gitHealthConnections : [];
133
69
  const sessionsOnTicket = sessions.filter((s) => s.workingOnTicket?.key === full.ticketKey);
@@ -153,9 +89,6 @@ async function buildTicketBundle(client, ticketKey) {
153
89
  }
154
90
  }
155
91
  lines.push('', '### Dependencies');
156
- // ORB-1614 - a cross-project blocker/dependent the caller cannot read
157
- // comes back with `title: null` - render a fixed, non-identifying
158
- // placeholder instead of the literal "null".
159
92
  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');
160
93
  lines.push('Blocked by:', fmtDeps(deps.blockedBy), 'Blocks:', fmtDeps(deps.blocks));
161
94
  if (unhealthy.length > 0) {
@@ -188,48 +121,23 @@ async function buildTicketBundle(client, ticketKey) {
188
121
  blockedBy: deps.blockedBy.map((e) => ({ ticketKey: e.ticketKey, title: e.title, statusName: e.statusName, external: e.external, resolved: e.resolved })),
189
122
  blocks: deps.blocks.map((e) => ({ ticketKey: e.ticketKey, title: e.title, statusName: e.statusName, external: e.external, resolved: e.resolved })),
190
123
  },
191
- // ORB-1697 - only the connections that need attention. A healthy
192
- // connection is 11 fields of "everything is fine" the agent never
193
- // acts on; the count preserves the fact that connections exist.
194
124
  gitHealth: { unhealthy, healthyCount: connections.length - unhealthy.length },
195
125
  activeSessions: sessionsOnTicket.map((s) => ({ userFullName: s.userFullName, userEmail: s.userEmail, status: s.status, lastSeenAt: s.lastSeenAt })),
196
126
  },
197
127
  };
198
128
  }
199
129
  export function makeSessionStartHandler(client) {
200
- // ORB-1607 - per-connection rules-hash cache. `buildOrbotoMcpServer`
201
- // calls this factory once per MCP connection, so this closure variable
202
- // lives for exactly that connection's lifetime and resets on reconnect.
203
- //
204
- // ORB-1697 - the defect that cache had: a stdio MCP server outlives
205
- // `/clear` and every context compaction, so a repeat call answered
206
- // "unchanged, keep following what you already loaded" at exactly the
207
- // moment the agent no longer held the rules. The connection is the wrong
208
- // thing to key the ack on; only the CALLER knows what is still in its
209
- // context. Hence `forceRules`, and an ack text that says so.
210
130
  let lastKnownRulesHash;
211
131
  return async (input = {}) => {
212
132
  const rulesParams = new URLSearchParams();
213
133
  if (input.projectId)
214
134
  rulesParams.set('projectId', input.projectId);
215
- // ORB-1753 - rule targeting: explicit input > env > (server-side) the
216
- // api-key's standing profile. The rules hash is profile-specific
217
- // server-side, so the cached ack stays valid per profile.
218
135
  applyAgentProfile(rulesParams, { agentKind: input.agentKind, modelTier: input.modelTier });
219
- // ORB-1818 - both full-text answers bypass the ack: the caller is
220
- // asking for the text precisely because it does not hold it.
221
136
  const wantsFullRules = input.forceRules === true || input.rulesOnly === true;
222
137
  if (lastKnownRulesHash && !wantsFullRules)
223
138
  rulesParams.set('knownRulesHash', lastKnownRulesHash);
224
139
  const rulesQs = rulesParams.toString();
225
140
  const rulesPath = rulesQs ? `/agent-instructions?${rulesQs}` : '/agent-instructions';
226
- // ORB-1818 - `rulesOnly`: the STATELESS way back from the index to the
227
- // full text. The `response_expand` handle the index also carries lives
228
- // in this process' memory (15 min, 16 payloads), so a server restart,
229
- // a reconnect or simply 16 later truncations can retire it - and a
230
- // binding rule must never become unreachable. This path re-reads the
231
- // rules from the API instead, costs one call, and skips the whole rest
232
- // of the digest. Protected in both halves: it IS the rule text.
233
141
  if (input.rulesOnly) {
234
142
  const rules = await loadRequiredRules(client, rulesPath);
235
143
  if (rules.rulesHash)
@@ -258,24 +166,14 @@ export function makeSessionStartHandler(client) {
258
166
  const [me, rules, assigned, timer, inboxRaw] = await Promise.all([
259
167
  client.get('/users/me').catch(() => null),
260
168
  loadRequiredRules(client, rulesPath, rulesParams.get('knownRulesHash') ?? undefined),
261
- // ORB-1330 - a re-orientation briefing must only list OPEN work.
262
- // Filter to in_progress + in_review so DONE tickets can't pose as
263
- // "what you're working on" at the moment the agent has the least
264
- // context and would otherwise re-claim / re-report finished work.
265
- // Cap 20.
266
169
  client.get('/users/me/assigned-tickets?statuses=IN_PROGRESS,IN_REVIEW&limit=20').catch(() => ({ items: [] })),
267
170
  client.get('/time/timer').catch(() => null),
268
- // ORB-1727 - unread agent messages, delivered right where an agent
269
- // re-orients. Coerced defensively (ORB-1697 lesson) and optional.
270
171
  client.get('/v1/agent/messages?limit=10')
271
172
  .then((r) => (Array.isArray(r?.messages) ? r.messages : []))
272
173
  .catch(() => []),
273
174
  ]);
274
175
  const pendingMessages = inboxRaw.map((m) => ({ id: m.id, fromUserId: m.fromUserId, kind: m.kind, subject: m.subject, createdAt: m.createdAt }));
275
176
  const tickets = Array.isArray(assigned) ? assigned : (assigned?.items ?? []);
276
- // ORB-1605 - git-connection health for every project the caller has
277
- // open work in right now. Cheap, computed, read-only (see
278
- // services/git-health.ts) - safe to fan out on every session start.
279
177
  const projectIds = [...new Set(tickets.map((t) => t.projectId).filter((id) => !!id))].slice(0, MAX_GIT_HEALTH_PROJECTS);
280
178
  const gitHealthByProject = await Promise.all(projectIds.map(async (projectId) => ({
281
179
  projectId,
@@ -292,13 +190,7 @@ export function makeSessionStartHandler(client) {
292
190
  const unhealthyWarnings = gitHealthWithConnections.flatMap((p) => p.connections
293
191
  .filter((c) => !c.healthy)
294
192
  .map((c) => `- Project ${p.projectId}: connection "${c.name}" (${c.provider}) is unhealthy - ${GIT_HEALTH_REASON_TEXT[c.reason ?? ''] ?? c.reason ?? 'unknown reason'}. If closing a ticket here depends on commit/PR ingestion, verify manually - ingestion may be stalled.`));
295
- // ORB-1607 - the optional one-shot ticket bundle, built after the
296
- // rest so it doesn't hold up the core digest on a slow primer render.
297
193
  const bundle = input.ticketKey ? await buildTicketBundle(client, input.ticketKey) : null;
298
- // ORB-1697 - when the caller named the ticket it is working, the
299
- // cross-project assigned list is noise: it measured 3.5k characters of
300
- // other projects' tickets on a real call. List the ones in the same
301
- // project, count the rest so nothing is hidden.
302
194
  const scopeProjectId = bundle?.projectId;
303
195
  const scopedTickets = scopeProjectId
304
196
  ? tickets.filter((t) => !t.projectId || t.projectId === scopeProjectId)
@@ -309,11 +201,6 @@ export function makeSessionStartHandler(client) {
309
201
  lines.push(`You are ${me.fullName ?? me.email}${me.email ? ` (${me.email})` : ''}.`);
310
202
  if (me?.workspaceLocale || me?.locale)
311
203
  lines.push(`Write tickets / comments / docs in: ${me.workspaceLocale ?? me.locale}.`);
312
- // ORB-1818 - three delivery modes for the rules, cheapest first:
313
- // ack - this connection already delivered this exact hash.
314
- // index - one line per rule + the hash + a handle; the default.
315
- // full - the text inline (forceRules, or an API too old to send
316
- // an index - never leave a caller without its rules).
317
204
  const rulesText = rules.instructions ?? '';
318
205
  const rulesIndex = Array.isArray(rules.rulesIndex)
319
206
  ? rules.rulesIndex.map((e) => e?.title).filter((t) => typeof t === 'string' && t.length > 0)
@@ -321,23 +208,14 @@ export function makeSessionStartHandler(client) {
321
208
  const deliverIndex = !rules.rulesUnchanged && input.forceRules !== true && rulesIndex.length > 0 && rulesText.length > 0;
322
209
  const deliverFull = !rules.rulesUnchanged && !deliverIndex && rulesText.length > 0;
323
210
  const rulesChars = rules.rulesChars ?? rulesText.length;
324
- // The way back, spelled out for the structured half too: Claude Code
325
- // keeps `structuredContent` and drops the text block, so a pointer
326
- // that lives only in the Markdown reaches half the clients.
327
211
  const HOW_TO_READ_RULES = 'Call orboto_session_start { rulesOnly: true } for the complete rule text (one call, never truncated). '
328
212
  + 'Do that whenever you do not already hold this exact rulesHash - after a compaction, a /clear, or as a fresh agent.';
329
213
  let rulesHandle;
330
214
  lines.push('', '## Working rules - follow these');
331
215
  if (rules.rulesUnchanged) {
332
- // ORB-1697 - never assert that the caller still HAS the rules. This
333
- // connection delivered them once, which says nothing about whether
334
- // they survived a compaction or a /clear on the client side.
335
216
  lines.push(`Unchanged since this connection last delivered them (hash ${rules.rulesHash}), so they were left out to save context.`, 'If the rules are NOT in your context right now - after a compaction, a /clear, or because you are a fresh agent on an existing connection - call this tool again with rulesOnly=true (rules alone) or forceRules=true (rules plus this digest) and read them in full. Do not proceed on a half-remembered rule set.');
336
217
  }
337
218
  else if (deliverIndex) {
338
- // The full text stays one call away in BOTH directions: statelessly
339
- // via rulesOnly, and via the standard ORB-1697 expand handle while
340
- // this process still holds it.
341
219
  rulesHandle = storePayload('orboto_session_start', {
342
220
  structuredContent: { rules: rules.instructions ?? '' },
343
221
  text: rules.instructions ?? '',
@@ -353,9 +231,6 @@ export function makeSessionStartHandler(client) {
353
231
  lines.push('No tickets currently assigned to you - claim or create one before you start coding.');
354
232
  else {
355
233
  for (const t of scopedTickets) {
356
- // ORB-1799 - the flag is the whole point of listing this ticket
357
- // again, so it goes on the SAME line, not into a separate section
358
- // a compaction-recovering agent might skim past.
359
234
  const landed = t.landedIdle
360
235
  ? ` - LANDED, IDLE ${t.landedIdleWorkingDays ?? '?'} working day(s): a commit is linked but the ticket never moved. Verify it is finished, then move it to review (orboto_work_finish / move_ticket) instead of re-implementing it.`
361
236
  : '';
@@ -380,14 +255,9 @@ export function makeSessionStartHandler(client) {
380
255
  lines.push(...bundle.lines);
381
256
  lines.push('', 'Re-run this after any context compaction to re-sync.');
382
257
  const result = {
383
- // ORB-1818 - when the full rule text IS the payload, neither half
384
- // may be cut; see PROTECT_TEXT_META. The index answer carries no
385
- // rule text and is budgeted like any other response.
386
258
  ...(deliverFull ? { _meta: { [PROTECT_TEXT_META]: true } } : {}),
387
259
  content: [{ type: 'text', text: lines.join('\n') }],
388
260
  structuredContent: {
389
- // ORB-1818 - empty in index/ack mode: the whole point is that the
390
- // text is not carried. `rulesDelivery` says which answer this is.
391
261
  rules: deliverFull ? (rules.instructions ?? '') : '',
392
262
  rulesHash: rules.rulesHash ?? null,
393
263
  rulesUnchanged: rules.rulesUnchanged === true,
@@ -400,15 +270,10 @@ export function makeSessionStartHandler(client) {
400
270
  ticketKey: t.ticketKey,
401
271
  title: t.title,
402
272
  status: t.statusName ?? t.status ?? null,
403
- // ORB-1799 - only carried when true; a `false` on every row is
404
- // budget spent on "nothing to see here" (see the MCP response
405
- // budget contract in CLAUDE.md).
406
273
  ...(t.landedIdle ? { landedIdle: true, landedIdleWorkingDays: t.landedIdleWorkingDays ?? null } : {}),
407
274
  })),
408
275
  ...(elsewhereCount > 0 ? { inProgressElsewhereCount: elsewhereCount } : {}),
409
276
  timer: timer?.ticketId ? { ticketKey: timer.ticketKey ?? null, startedAt: timer.startedAt ?? null } : null,
410
- // ORB-1697 - unhealthy connections only; a healthy one is 11 fields
411
- // the agent never acts on. `healthyCount` keeps the fact visible.
412
277
  gitHealth: {
413
278
  unhealthy: gitHealthWithConnections
414
279
  .map((p) => ({ projectId: p.projectId, connections: p.connections.filter((c) => !c.healthy) }))
@@ -54,9 +54,6 @@ describe('orboto_session_start (ORB-1093)', () => {
54
54
  expect(text).toContain('Running on ORB-42');
55
55
  expect(calls.some((c) => c.startsWith('/agent-instructions'))).toBe(true);
56
56
  expect(calls.some((c) => c.startsWith('/time/timer'))).toBe(true);
57
- // ORB-1330 - the briefing must only ask for OPEN work; DONE tickets
58
- // must never reach the "in-progress" section. Assert the status
59
- // filter is on the wire so a future refactor can't silently widen it.
60
57
  const assignedCall = calls.find((c) => c.startsWith('/users/me/assigned-tickets'));
61
58
  expect(assignedCall).toBeDefined();
62
59
  expect(assignedCall).toContain('statuses=IN_PROGRESS,IN_REVIEW');
@@ -73,9 +70,6 @@ describe('orboto_session_start (ORB-1093)', () => {
73
70
  expect(text).toContain('No tickets currently assigned');
74
71
  expect(text).toContain('No timer running');
75
72
  });
76
- // ORB-1605 - session-start fans out to GET /projects/:id/git-health for
77
- // every distinct project the caller has open work in, and surfaces a
78
- // warning when a connection is unhealthy.
79
73
  it('warns when a project git connection is unhealthy', async () => {
80
74
  stubByPath({
81
75
  '/users/me/assigned-tickets': {
@@ -96,8 +90,6 @@ describe('orboto_session_start (ORB-1093)', () => {
96
90
  expect(text).toContain('Git connection health - WARNING');
97
91
  expect(text).toContain('orboto/orboto');
98
92
  expect(text).toContain('connection is deactivated');
99
- // ORB-1697 - unhealthy connections only; a healthy one is 11 fields
100
- // the agent never acts on, so it is reduced to a count.
101
93
  const structured = res.structuredContent;
102
94
  expect(structured.gitHealth.unhealthy).toHaveLength(1);
103
95
  expect(structured.gitHealth.unhealthy[0].projectId).toBe('proj-1');
@@ -123,10 +115,6 @@ describe('orboto_session_start (ORB-1093)', () => {
123
115
  expect(text).not.toContain('Git connection health - WARNING');
124
116
  });
125
117
  });
126
- // ORB-1607 - rules-hash ack: the handler remembers the last rulesHash it
127
- // saw FOR THE LIFETIME OF ONE `makeSessionStartHandler(client)` CLOSURE
128
- // (mirrors one MCP connection) and passes it back as `knownRulesHash` on
129
- // every subsequent call.
130
118
  describe('orboto_session_start - rules-hash ack (ORB-1607)', () => {
131
119
  it('sends no knownRulesHash on the first call, then passes the received hash back on the second', async () => {
132
120
  const calls = [];
@@ -162,9 +150,6 @@ describe('orboto_session_start - rules-hash ack (ORB-1607)', () => {
162
150
  const secondCall = calls.find((c) => c.startsWith('/agent-instructions'));
163
151
  expect(secondCall).toBe('/agent-instructions?knownRulesHash=abc123def456');
164
152
  const secondText = second.content[0].text;
165
- // ORB-1697 - the ack must not assert that the CALLER still holds the
166
- // rules (a stdio server outlives /clear and every compaction), and it
167
- // must name the way to get them back.
168
153
  expect(secondText).toContain('Unchanged since this connection last delivered them');
169
154
  expect(secondText).toContain('forceRules=true');
170
155
  expect(secondText).not.toContain('keep following what you already loaded');
@@ -185,9 +170,6 @@ describe('orboto_session_start - rules-hash ack (ORB-1607)', () => {
185
170
  expect(call).toBe('/agent-instructions');
186
171
  });
187
172
  });
188
- // ORB-1607 - the optional `ticketKey` one-shot bundle: project primer,
189
- // full ticket (incl. dependencies + checklists), git health, and active
190
- // sessions folded into the same response.
191
173
  describe('orboto_session_start - ticketKey bundle (ORB-1607)', () => {
192
174
  const bundleStubs = {
193
175
  '/projects/by-key/ORB': { id: 'proj-1', key: 'ORB', name: 'orboto' },
@@ -212,7 +194,6 @@ describe('orboto_session_start - ticketKey bundle (ORB-1607)', () => {
212
194
  '/users/me': { email: 'dev@x.io', fullName: 'Dev' },
213
195
  '/agent-instructions': { instructions: 'rules here', rulesHash: 'h1' },
214
196
  '/time/timer': {},
215
- // Re-fetch of the enriched by-id row (mirrors orboto_get_ticket).
216
197
  '/projects/proj-1/tickets/tick-1': {
217
198
  id: 'tick-1', projectId: 'proj-1', ticketKey: 'ORB-42', title: 'Bug',
218
199
  status: 'IN_PROGRESS', statusName: 'In Progress', priority: 'high', type: 'bug',
@@ -220,10 +201,6 @@ describe('orboto_session_start - ticketKey bundle (ORB-1607)', () => {
220
201
  },
221
202
  };
222
203
  it('folds primer + ticket + dependencies + checklists + git health + active sessions into one response', async () => {
223
- // bundleStubs first - the merged map is matched by `startsWith`, and
224
- // baseStubs' bare `/projects/proj-1/tickets/tick-1` would otherwise
225
- // shadow bundleStubs' longer `/projects/proj-1/tickets/tick-1/dependencies`
226
- // if it came first in iteration order.
227
204
  const calls = stubByPath({ ...bundleStubs, ...baseStubs });
228
205
  const res = await makeSessionStartHandler(client)({ ticketKey: 'ORB-42' });
229
206
  const text = res.content[0].text;
@@ -239,7 +216,6 @@ describe('orboto_session_start - ticketKey bundle (ORB-1607)', () => {
239
216
  expect(structured.ticketBundle.checklists).toHaveLength(1);
240
217
  expect(structured.ticketBundle.dependencies.blockedBy).toHaveLength(1);
241
218
  expect(structured.ticketBundle.activeSessions).toHaveLength(1);
242
- // Replaces what would otherwise be several separate calls.
243
219
  expect(calls.some((c) => c.startsWith('/projects/proj-1/ai-primer'))).toBe(true);
244
220
  expect(calls.some((c) => c.startsWith('/tickets/tick-1/checklists'))).toBe(true);
245
221
  expect(calls.some((c) => c.startsWith('/projects/proj-1/tickets/tick-1/dependencies'))).toBe(true);
@@ -250,7 +226,6 @@ describe('orboto_session_start - ticketKey bundle (ORB-1607)', () => {
250
226
  const text = res.content[0].text;
251
227
  expect(text).toContain('## Ticket bundle: ORB-999');
252
228
  expect(text).toContain('Could not load this ticket');
253
- // The rest of the digest (rules, in-progress work, timer) still renders.
254
229
  expect(text).toContain('## Working rules');
255
230
  expect(text).toContain('## Timer');
256
231
  const structured = res.structuredContent;
@@ -275,10 +250,9 @@ describe('orboto_session_start - context cost (ORB-1697)', () => {
275
250
  it('forceRules re-sends the rules in full even when this connection already delivered them', async () => {
276
251
  const calls = stubByPath(rulesStub);
277
252
  const handler = makeSessionStartHandler(client);
278
- await handler(); // primes the per-connection hash
253
+ await handler();
279
254
  calls.length = 0;
280
255
  const forced = await handler({ forceRules: true });
281
- // No knownRulesHash means the API cannot answer with an ack.
282
256
  expect(calls.find((c) => c.startsWith('/agent-instructions'))).toBe('/agent-instructions');
283
257
  const text = forced.content[0].text;
284
258
  expect(text).toContain('THE FULL RULE TEXT');
@@ -312,7 +286,6 @@ describe('orboto_session_start - context cost (ORB-1697)', () => {
312
286
  const structured = res.structuredContent;
313
287
  expect(structured.gitHealth.unhealthy).toEqual([]);
314
288
  expect(structured.gitHealth.healthyCount).toBe(1);
315
- // And the connection object itself is nowhere in the payload.
316
289
  expect(JSON.stringify(structured)).not.toContain('lastProbeAt');
317
290
  });
318
291
  it('with a ticketKey, open work in OTHER projects becomes a count instead of 20 rows', async () => {
@@ -323,7 +296,6 @@ describe('orboto_session_start - context cost (ORB-1697)', () => {
323
296
  id: 'tick-1', projectId: 'proj-1', ticketKey: 'ORB-42', title: 'Bug',
324
297
  status: 'IN_PROGRESS', statusName: 'In Progress', priority: 'high', type: 'bug',
325
298
  },
326
- // Longest path first - the stub map is matched by startsWith.
327
299
  '/users/me/assigned-tickets': {
328
300
  items: [
329
301
  { ticketKey: 'ORB-42', title: 'Same project', statusName: 'In Progress', projectId: 'proj-1' },
@@ -341,7 +313,6 @@ describe('orboto_session_start - context cost (ORB-1697)', () => {
341
313
  expect(structured.inProgress.map((t) => t.ticketKey)).toEqual(['ORB-42']);
342
314
  expect(structured.inProgressElsewhereCount).toBe(2);
343
315
  const text = res.content[0].text;
344
- // Nothing is hidden: the count is stated, with the way to list them.
345
316
  expect(text).toContain('2 open ticket(s) assigned to you in other projects');
346
317
  expect(text).toContain('orboto_my_tickets');
347
318
  expect(text).not.toContain('Another project entirely');
@@ -376,8 +347,6 @@ describe('orboto_session_start - context cost (ORB-1697)', () => {
376
347
  describe('orboto_session_start - a 200 with an unexpected body never kills the digest (ORB-1697)', () => {
377
348
  const client = new OrbotoClient({ baseUrl: 'http://api.test', apiKey: 'orb_k' });
378
349
  it('renders the digest when every optional endpoint answers 200 with {}', async () => {
379
- // Only the paths the digest cannot work without return real data; every
380
- // optional read answers `{}`, which is what the fallthrough produces.
381
350
  stubByPath({
382
351
  '/projects/by-key/ORB': { id: 'proj-1', key: 'ORB', name: 'orboto' },
383
352
  '/projects/proj-1/tickets/by-key/42': { id: 'tick-1', projectId: 'proj-1', ticketKey: 'ORB-42', title: 'Bug' },
@@ -417,14 +386,12 @@ describe('ORB-1753 - rule targeting passthrough', () => {
417
386
  calls = stubByPath({ '/agent-instructions': { instructions: 'RULES', rulesHash: 'h2' } });
418
387
  handler = makeSessionStartHandler(client);
419
388
  await handler({});
420
- // env applies, lowercase-normalized client-side.
421
389
  expect(calls.find((c) => c.startsWith('/agent-instructions'))).toContain('agentKind=coding');
422
390
  expect(calls.find((c) => c.startsWith('/agent-instructions'))).toContain('modelTier=frontier');
423
391
  vi.restoreAllMocks();
424
392
  calls = stubByPath({ '/agent-instructions': { instructions: 'RULES', rulesHash: 'h3' } });
425
393
  handler = makeSessionStartHandler(client);
426
394
  await handler({ agentKind: 'reviewer' });
427
- // explicit input beats the env default.
428
395
  expect(calls.find((c) => c.startsWith('/agent-instructions'))).toContain('agentKind=reviewer');
429
396
  });
430
397
  });
@@ -460,22 +427,16 @@ describe('ORB-1818 - rules index instead of the full rule text', () => {
460
427
  stubDigest(rulesFull);
461
428
  const res = await makeSessionStartHandler(client)();
462
429
  const budgeted = applyResponseBudget('orboto_session_start', res, {});
463
- // The whole point: no truncation, because there is nothing big left.
464
430
  expect(budgeted.truncatedChars).toBe(0);
465
431
  expect(budgeted.responseChars).toBeLessThan(DEFAULT_BUDGET_CHARS);
466
- // ...and the tool no longer buys itself a special budget.
467
432
  expect(budgetFor('orboto_session_start', {})).toBe(DEFAULT_BUDGET_CHARS);
468
433
  const structured = res.structuredContent;
469
434
  expect(structured.rulesDelivery).toBe('index');
470
435
  expect(structured.rules).toBe('');
471
- // One line per enabled block, in delivery order.
472
436
  expect(structured.rulesIndex).toEqual(BLOCK_TITLES);
473
437
  expect(structured.rulesChars).toBe(RULES_TEXT.length);
474
- // The way back must live in the STRUCTURED half too - Claude Code
475
- // keeps that one and drops the text block.
476
438
  expect(structured.rulesHowToRead).toContain('rulesOnly');
477
439
  expect(structured.rulesHandle).toBeTruthy();
478
- // The rest of the digest is unchanged (ORB-1799 landed-idle included).
479
440
  expect(structured.inProgress).toHaveLength(2);
480
441
  expect(structured.inProgress[1].landedIdle).toBe(true);
481
442
  expect(structured.timer?.ticketKey).toBe('ORB-42');
@@ -527,13 +488,9 @@ describe('ORB-1818 - rules index instead of the full rule text', () => {
527
488
  expect(res.structuredContent.rulesDelivery).toBe('full');
528
489
  const budgeted = applyResponseBudget('orboto_session_start', res, {});
529
490
  expect(budgeted.truncatedChars).toBe(0);
530
- // Both halves survive: the structured one via PROTECTED_PATHS, the
531
- // text one via the per-call PROTECT_TEXT_META flag...
532
491
  expect(budgeted.result.structuredContent.rules).toBe(RULES_TEXT);
533
492
  expect(budgeted.result.content[0].text).toContain(RULES_TEXT);
534
- // ...and no misleading "response truncated" notice is appended.
535
493
  expect(budgeted.result.content[0].text).not.toContain('Response truncated');
536
- // The marker itself never reaches the wire.
537
494
  expect(budgeted.result._meta).toBeUndefined();
538
495
  });
539
496
  it('rulesOnly returns just the rules - no tickets, no timer, no other calls', async () => {
@@ -1,23 +1,7 @@
1
1
  /**
2
2
  * ORB-799 (gap-cluster 8) - re-parent existing tickets.
3
3
  *
4
- * `orboto_update_ticket` accepts most ticket fields but NOT
5
- * `parentTicketId` / `parentTicketKey`, mirroring an API choice made
6
- * during ORB-309: parent assignment was treated as a creation-time
7
- * decision (set via `orboto_create_ticket`'s `parentTicketKey`) and the
8
- * patch surface was deliberately narrowed.
9
- *
10
- * In practice that left a gap: re-parenting an existing ticket under a
11
- * new epic (e.g. discovering during cluster-formation that ORB-600
12
- * belongs under ORB-800 as a sub-ticket) had no MCP-side path and
13
- * required falling back to the Bash wrapper's raw `patch /projects/...`
14
- * route - documented in `feedback_mcp_reparent_limitation.md`.
15
- *
16
- * Design choice: separate `orboto_set_parent` tool rather than
17
- * extending `orboto_update_ticket`'s patch shape. Symmetric to
18
- * `orboto_set_milestone` which exists for the same reason - a
19
- * dedicated tool with clearer semantics, easier permission gating
20
- * thinking, and a natural `parentTicketKey: null` path for detaching.
4
+ * @see ORB-309, ORB-600, ORB-800
21
5
  */
22
6
  import { z } from 'zod';
23
7
  import { OrbotoApiError } from '../orboto-client.js';
@@ -52,8 +36,6 @@ export function makeSetParentHandler(client) {
52
36
  updated = await client.patch(`/projects/${ticket.projectId}/tickets/${ticket.id}`, { parentTicketId });
53
37
  }
54
38
  catch (err) {
55
- // Surface the API's own cycle-detection error verbatim if it
56
- // catches a deeper cycle we couldn't see from one hop above.
57
39
  if (err instanceof OrbotoApiError && err.status === 400) {
58
40
  throw new Error(`Re-parent rejected by the API: ${err.body || 'cycle or constraint violation'}.`);
59
41
  }
@@ -22,7 +22,6 @@ export async function resolveProjectByKey(client, key) {
22
22
  export function agentTicketListRow(t, verbose = false) {
23
23
  if (verbose) {
24
24
  return {
25
- // ORB-1179 - uuid for write tools that want it without a lookup.
26
25
  id: t.id,
27
26
  key: t.ticketKey,
28
27
  title: t.title,
@@ -41,9 +40,6 @@ export function agentTicketListRow(t, verbose = false) {
41
40
  ...(t.waitingForGitIngestion ? { waitingForGitIngestion: true } : {}),
42
41
  };
43
42
  }
44
- // Lean row: fields at their DEFAULT value are omitted entirely - a
45
- // reader treats absence as "task / normal / no due date / unassigned".
46
- // The list is a decision aid; the full picture is one get_ticket away.
47
43
  const assigneeNames = (t.assignees ?? []).map((a) => a.fullName || a.email);
48
44
  return {
49
45
  key: t.ticketKey,
@@ -53,7 +49,6 @@ export function agentTicketListRow(t, verbose = false) {
53
49
  ...(t.type && t.type !== 'task' ? { type: t.type } : {}),
54
50
  ...(t.dueDate ? { dueDate: t.dueDate } : {}),
55
51
  ...(assigneeNames.length > 0 ? { assigneeNames } : {}),
56
- // ORB-1605 - only present when it fires; absent costs zero chars.
57
52
  ...(t.waitingForGitIngestion ? { waitingForGitIngestion: true } : {}),
58
53
  };
59
54
  }
@@ -92,25 +87,10 @@ export function ticketLine(t) {
92
87
  if (t.assignees && t.assignees.length > 0) {
93
88
  parts.push(`→ ${t.assignees.map((a) => a.fullName || a.email).join(', ')}`);
94
89
  }
95
- // ORB-1605 - flag a ticket that's genuinely just waiting on stalled
96
- // commit/PR ingestion, not a ticket someone forgot to close.
97
90
  if (t.waitingForGitIngestion)
98
91
  parts.push('[waiting on Git ingestion]');
99
92
  return parts.join(' ');
100
93
  }
101
- // ---------------------------------------------------------------------------
102
- // ORB-1252 / ORB-1283 / ORB-1609 - the agent-instance token.
103
- //
104
- // One MCP server process = one agent instance. Every surface that scopes work
105
- // to an instance (timers, and since ORB-1609 work-session leases) MUST derive
106
- // the token the same way, or the same agent ends up in two lanes: a claim
107
- // starting a timer in lane A while its work session holds the lease in lane B
108
- // is precisely the class of bug ORB-1603 had to paper over.
109
- //
110
- // A per-process random UUID rather than the PID: PIDs are recycled by the OS,
111
- // so a fresh process could inherit a recycled PID and adopt a previous
112
- // instance's stale timer or lease.
113
- // ---------------------------------------------------------------------------
114
94
  import { randomUUID } from 'node:crypto';
115
95
  const MCP_PROCESS_INSTANCE = `mcp-${randomUUID()}`;
116
96
  /** Precedence: explicit caller-supplied token > per-connection MCP session id
@@ -158,27 +138,6 @@ export function sizeBlockResult(err, verb) {
158
138
  isError: true,
159
139
  };
160
140
  }
161
- // ---------------------------------------------------------------------------
162
- // ORB-1826 - shared name-matching normaliser for free-text milestone /
163
- // label / status resolvers. An agent surface can hand back a name that
164
- // differs from the canonical DB row only in mechanically-recoverable ways:
165
- // leftover HTML entities from a rendering step upstream of the agent
166
- // (`QA & Testing`), a different case, or extra/collapsed whitespace.
167
- // The workspace rule is normalise, never reject - if the canonical form is
168
- // mechanically derivable, derive it instead of rejecting the call.
169
- //
170
- // Resolution order for every call site that uses `resolveByName`: raw exact
171
- // match first (zero-cost, preserves today's behaviour for clean names),
172
- // then a UNIQUE normalised match, then the existing ambiguity error listing
173
- // the candidates.
174
- //
175
- // Twin: `apps/api/src/lib/name-normalize.ts` (same two functions, same
176
- // semantics). This package ships standalone to npm with a deliberately
177
- // minimal dependency set (MCP SDK + zod only) and does not depend on the
178
- // API workspace package, so the ~20 lines below are duplicated rather than
179
- // imported. The Go CLI (`cli/internal/cmd/normalize.go`) carries a third
180
- // copy for the same reason. Keep all three in sync.
181
- // ---------------------------------------------------------------------------
182
141
  const NAMED_HTML_ENTITIES = {
183
142
  amp: '&',
184
143
  lt: '<',
@@ -91,7 +91,6 @@ describe('ticketLine', () => {
91
91
  });
92
92
  expect(line).toBe('[ACME-2] Docs update (To Do)');
93
93
  });
94
- // ORB-1605 - flags the stalled-ingestion signal in the one-line summary.
95
94
  it('appends the waiting-on-ingestion marker when waitingForGitIngestion is true', () => {
96
95
  const line = ticketLine({
97
96
  id: 't1', projectId: 'p1', milestoneId: null,
@@ -117,9 +116,6 @@ describe('ticketLine', () => {
117
116
  expect(line).toBe('[ACME-4] Docs change (In Review)');
118
117
  });
119
118
  });
120
- // ORB-1826 - production MCP error log showed `Milestone "QA &amp; Testing"
121
- // not found` for a milestone literally named "QA & Testing": some upstream
122
- // surface handed the agent an HTML-escaped name. "Normalise, never reject."
123
119
  describe('normalizeName', () => {
124
120
  it('decodes the five XML-safe named entities', () => {
125
121
  expect(normalizeName('QA &amp; Testing')).toBe('qa & testing');
@@ -1,23 +1,8 @@
1
1
  /**
2
2
  * ORB-1693 - the agent-facing projection of a duplicate-warning entry.
3
- *
4
- * Measured: `similarWarnings` was 76% of the entire create_ticket response
5
- * volume (~329k tokens over the corpus), and most of each entry was
6
- * decision-irrelevant: a CSS hex colour, a UUID next to the key, a
7
- * redundant statusName next to statusCategory, and similarity at 15
8
- * decimal places. The agent decision needs exactly:
9
- *
10
- * { ticketKey, title, statusCategory, similarity (2dp), relation }
11
- *
12
- * The rich shape stays on the REST response for the web UI - this trims
13
- * only what agent surfaces emit. Same projection everywhere a warning
14
- * reaches an agent: create_ticket, the duplicate-block 409, check_similar
15
- * (mirrored in the chat registry + skill wrapper - keep them in sync).
16
3
  */
17
4
  export function trimSimilarEntry(w) {
18
5
  return {
19
- // Keys over UUIDs (repo rule); the short-id fallback only exists for
20
- // pre-backfill rows that never got a key.
21
6
  ticketKey: w.ticketKey ?? w.id.slice(0, 8),
22
7
  title: w.title,
23
8
  statusCategory: w.statusCategory ?? null,