@orboto/mcp 0.192.0 → 0.194.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/event-bridge.js +1 -47
- package/dist/event-bridge.test.js +0 -19
- package/dist/http-transport-binding.test.js +1 -4
- package/dist/http-transport.js +1 -213
- package/dist/http-transport.test.js +0 -56
- package/dist/index.js +1 -48
- package/dist/input-schema.js +2 -76
- package/dist/input-schema.test.js +1 -15
- package/dist/instructions-budget.test.js +0 -2
- package/dist/manifest-size.test.js +3 -72
- package/dist/oauth-bootstrap.js +0 -47
- package/dist/oauth-bootstrap.test.js +4 -16
- package/dist/orboto-client.js +0 -30
- package/dist/orboto-client.test.js +0 -12
- package/dist/prompts.js +0 -39
- package/dist/protect-text-usage.test.js +1 -9
- package/dist/required-rules-wrapper.test.js +0 -5
- package/dist/required-rules.js +0 -2
- package/dist/resources.js +0 -62
- package/dist/resources.test.js +0 -16
- package/dist/response-budget.js +2 -150
- package/dist/response-budget.test.js +1 -32
- package/dist/server.js +4 -180
- package/dist/session-nudge.js +0 -9
- package/dist/session-nudge.test.js +2 -10
- package/dist/session-nudge.transport.test.js +0 -23
- package/dist/tool-annotations.test.js +0 -44
- package/dist/tool-docs.js +1 -12
- package/dist/tool-docs.test.js +2 -8
- package/dist/tool-parameters.test.js +0 -19
- package/dist/tools/absence-writes.js +1 -17
- package/dist/tools/admin-translations.js +0 -11
- package/dist/tools/admin-writes.js +0 -28
- package/dist/tools/admin-writes.test.js +0 -1
- package/dist/tools/agent-coordination.js +2 -24
- package/dist/tools/agent-coordination.test.js +0 -2
- package/dist/tools/agent-instructions.js +0 -10
- package/dist/tools/agent-instructions.test.js +0 -2
- package/dist/tools/agent-messages.js +0 -1
- package/dist/tools/ai-status.js +0 -14
- package/dist/tools/ai-usage.js +0 -10
- package/dist/tools/analytics.js +0 -2
- package/dist/tools/api-call.js +0 -12
- package/dist/tools/api-call.test.js +1 -2
- package/dist/tools/api-search.js +1 -6
- package/dist/tools/approvals.js +0 -6
- package/dist/tools/attach.js +0 -25
- package/dist/tools/backup.js +0 -13
- package/dist/tools/backup.test.js +1 -2
- package/dist/tools/bulk-create.js +1 -25
- package/dist/tools/bulk-create.test.js +0 -4
- package/dist/tools/bulk-writes.js +69 -61
- package/dist/tools/bulk-writes.test.js +34 -4
- package/dist/tools/check-similar.js +0 -17
- package/dist/tools/checklist-writes.js +82 -27
- package/dist/tools/checklist-writes.test.js +69 -1
- package/dist/tools/claim.js +1 -71
- package/dist/tools/claim.test.js +0 -2
- package/dist/tools/critical-path.js +0 -5
- package/dist/tools/critical-path.test.js +0 -3
- package/dist/tools/cross-project-links.ee.js +1 -24
- package/dist/tools/doc-attachments.js +0 -28
- package/dist/tools/doc-attachments.test.js +1 -4
- package/dist/tools/doc-comments.js +1 -37
- package/dist/tools/doc-comments.test.js +1 -2
- package/dist/tools/doc-edits.js +1 -31
- package/dist/tools/doc-edits.test.js +0 -9
- package/dist/tools/doc-export.js +0 -24
- package/dist/tools/doc-export.test.js +0 -12
- package/dist/tools/doc-revisions.js +0 -23
- package/dist/tools/docs-ai.js +0 -34
- package/dist/tools/docs-ai.test.js +0 -11
- package/dist/tools/docs.js +1 -75
- package/dist/tools/docs.test.js +1 -3
- package/dist/tools/embedding-status.js +0 -13
- package/dist/tools/embedding-status.test.js +0 -1
- package/dist/tools/feedback.js +1 -15
- package/dist/tools/feedback.test.js +0 -1
- package/dist/tools/get-checklists.js +6 -14
- package/dist/tools/get-project-primer.js +1 -25
- package/dist/tools/get-project.js +0 -17
- package/dist/tools/get-ticket.js +1 -61
- package/dist/tools/get-timer.js +0 -11
- package/dist/tools/git-health-reasons.js +0 -1
- package/dist/tools/identity.js +0 -13
- package/dist/tools/list-projects.js +0 -20
- package/dist/tools/list-projects.test.js +0 -1
- package/dist/tools/list-tickets.js +10 -9
- package/dist/tools/milestones-crud.test.js +0 -4
- package/dist/tools/milestones.js +0 -43
- package/dist/tools/my-tickets.js +0 -7
- package/dist/tools/phase-b-tools.test.js +14 -21
- package/dist/tools/primer-facts.js +1 -49
- package/dist/tools/primer-facts.test.js +0 -20
- package/dist/tools/project-listings.js +0 -25
- package/dist/tools/query.js +1 -12
- package/dist/tools/query.test.js +0 -8
- package/dist/tools/raci.js +0 -11
- package/dist/tools/requirements-spec.js +1 -12
- package/dist/tools/response-expand.js +0 -19
- package/dist/tools/review-policy.js +0 -24
- package/dist/tools/review-policy.test.js +0 -2
- package/dist/tools/search.js +1 -13
- package/dist/tools/session-start.js +1 -136
- package/dist/tools/session-start.test.js +1 -44
- package/dist/tools/set-parent.js +1 -19
- package/dist/tools/shared.js +0 -41
- package/dist/tools/shared.test.js +0 -4
- package/dist/tools/similar-projection.js +0 -15
- package/dist/tools/ticket-attachments.js +1 -25
- package/dist/tools/ticket-schedules.js +0 -9
- package/dist/tools/ticket-schedules.test.js +0 -1
- package/dist/tools/ticket-writes.js +1 -110
- package/dist/tools/ticket-writes.test.js +1 -15
- package/dist/tools/time-writes.js +26 -46
- package/dist/tools/time-writes.test.js +18 -0
- package/dist/tools/update-project.js +1 -27
- package/dist/tools/update-project.test.js +1 -1
- package/dist/tools/wiki.js +0 -8
- package/dist/tools/work-next.js +0 -5
- package/dist/tools/work-sessions-start.test.js +0 -2
- package/dist/tools/work-sessions.js +0 -48
- package/dist/tools/work-sessions.test.js +0 -5
- package/dist/toolset.js +2 -81
- package/dist/toolset.test.js +0 -20
- package/dist/version-guard.test.js +0 -4
- package/dist/version.js +0 -12
- package/dist/with-metrics-output-schema.test.js +1 -14
- package/dist/with-metrics.js +2 -74
- package/dist/with-metrics.test.js +7 -29
- package/package.json +1 -1
|
@@ -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 -
|
|
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();
|
|
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 () => {
|
package/dist/tools/set-parent.js
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-799 (gap-cluster 8) - re-parent existing tickets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
}
|
package/dist/tools/shared.js
CHANGED
|
@@ -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 & 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 & 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,
|