@orboto/mcp 0.178.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 (159) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +123 -0
  3. package/dist/event-bridge.js +250 -0
  4. package/dist/event-bridge.test.js +165 -0
  5. package/dist/http-transport-binding.test.js +151 -0
  6. package/dist/http-transport.js +608 -0
  7. package/dist/http-transport.test.js +408 -0
  8. package/dist/index.js +128 -0
  9. package/dist/input-schema.js +293 -0
  10. package/dist/input-schema.test.js +198 -0
  11. package/dist/instructions-budget.test.js +34 -0
  12. package/dist/manifest-size.test.js +192 -0
  13. package/dist/mcp-instrument.js +24 -0
  14. package/dist/oauth-bootstrap.js +413 -0
  15. package/dist/oauth-bootstrap.test.js +212 -0
  16. package/dist/orboto-client.js +250 -0
  17. package/dist/orboto-client.test.js +141 -0
  18. package/dist/prompts.js +219 -0
  19. package/dist/prompts.test.js +79 -0
  20. package/dist/protect-text-usage.test.js +38 -0
  21. package/dist/resources.js +260 -0
  22. package/dist/resources.test.js +191 -0
  23. package/dist/response-budget.js +515 -0
  24. package/dist/response-budget.test.js +343 -0
  25. package/dist/server.js +547 -0
  26. package/dist/session-nudge.js +81 -0
  27. package/dist/session-nudge.test.js +100 -0
  28. package/dist/session-nudge.transport.test.js +100 -0
  29. package/dist/tool-annotations.test.js +97 -0
  30. package/dist/tool-docs.js +64 -0
  31. package/dist/tool-docs.test.js +90 -0
  32. package/dist/tool-parameters.test.js +145 -0
  33. package/dist/tools/absence-writes.js +93 -0
  34. package/dist/tools/absence-writes.test.js +106 -0
  35. package/dist/tools/admin-translations.js +83 -0
  36. package/dist/tools/admin-writes.js +174 -0
  37. package/dist/tools/admin-writes.test.js +120 -0
  38. package/dist/tools/agent-coordination.js +160 -0
  39. package/dist/tools/agent-coordination.test.js +156 -0
  40. package/dist/tools/agent-drift.js +79 -0
  41. package/dist/tools/agent-drift.test.js +87 -0
  42. package/dist/tools/agent-instructions.js +172 -0
  43. package/dist/tools/agent-instructions.test.js +124 -0
  44. package/dist/tools/agent-messages.js +75 -0
  45. package/dist/tools/ai-status.js +55 -0
  46. package/dist/tools/ai-status.test.js +43 -0
  47. package/dist/tools/ai-usage.js +68 -0
  48. package/dist/tools/analytics.js +72 -0
  49. package/dist/tools/analytics.test.js +71 -0
  50. package/dist/tools/api-call.js +72 -0
  51. package/dist/tools/api-call.test.js +104 -0
  52. package/dist/tools/api-search.js +77 -0
  53. package/dist/tools/api-search.test.js +90 -0
  54. package/dist/tools/approvals.js +68 -0
  55. package/dist/tools/approvals.test.js +48 -0
  56. package/dist/tools/attach.js +108 -0
  57. package/dist/tools/attach.test.js +117 -0
  58. package/dist/tools/backup.js +93 -0
  59. package/dist/tools/backup.test.js +108 -0
  60. package/dist/tools/bulk-create.js +213 -0
  61. package/dist/tools/bulk-create.test.js +135 -0
  62. package/dist/tools/bulk-writes.js +339 -0
  63. package/dist/tools/bulk-writes.test.js +239 -0
  64. package/dist/tools/check-similar.js +84 -0
  65. package/dist/tools/check-similar.test.js +101 -0
  66. package/dist/tools/checklist-writes.js +202 -0
  67. package/dist/tools/checklist-writes.test.js +184 -0
  68. package/dist/tools/claim.js +236 -0
  69. package/dist/tools/claim.test.js +207 -0
  70. package/dist/tools/critical-path.js +83 -0
  71. package/dist/tools/critical-path.test.js +114 -0
  72. package/dist/tools/cross-project-links.ee.js +124 -0
  73. package/dist/tools/cross-project-links.ee.test.js +163 -0
  74. package/dist/tools/customer-report.js +88 -0
  75. package/dist/tools/customer-report.test.js +55 -0
  76. package/dist/tools/doc-attachments.js +184 -0
  77. package/dist/tools/doc-attachments.test.js +136 -0
  78. package/dist/tools/doc-comments.js +228 -0
  79. package/dist/tools/doc-comments.test.js +192 -0
  80. package/dist/tools/doc-edits.js +229 -0
  81. package/dist/tools/doc-edits.test.js +218 -0
  82. package/dist/tools/doc-export.js +87 -0
  83. package/dist/tools/doc-export.test.js +109 -0
  84. package/dist/tools/doc-revisions.js +133 -0
  85. package/dist/tools/doc-revisions.test.js +103 -0
  86. package/dist/tools/docs-ai.js +171 -0
  87. package/dist/tools/docs-ai.test.js +160 -0
  88. package/dist/tools/docs.js +557 -0
  89. package/dist/tools/docs.test.js +365 -0
  90. package/dist/tools/embedding-status.js +100 -0
  91. package/dist/tools/embedding-status.test.js +86 -0
  92. package/dist/tools/free-busy.js +49 -0
  93. package/dist/tools/free-busy.test.js +46 -0
  94. package/dist/tools/get-checklists.js +72 -0
  95. package/dist/tools/get-project-primer.js +58 -0
  96. package/dist/tools/get-project.js +77 -0
  97. package/dist/tools/get-ticket.js +321 -0
  98. package/dist/tools/get-timer.js +62 -0
  99. package/dist/tools/help.js +44 -0
  100. package/dist/tools/identity.js +60 -0
  101. package/dist/tools/identity.test.js +67 -0
  102. package/dist/tools/list-git-app-installations.js +45 -0
  103. package/dist/tools/list-projects.js +72 -0
  104. package/dist/tools/list-projects.test.js +67 -0
  105. package/dist/tools/list-tickets.js +86 -0
  106. package/dist/tools/milestones-crud.test.js +154 -0
  107. package/dist/tools/milestones.js +279 -0
  108. package/dist/tools/milestones.test.js +60 -0
  109. package/dist/tools/my-tickets.js +65 -0
  110. package/dist/tools/personal-facts.js +72 -0
  111. package/dist/tools/personal-facts.test.js +45 -0
  112. package/dist/tools/phase-b-tools.test.js +530 -0
  113. package/dist/tools/portfolio.js +56 -0
  114. package/dist/tools/primer-facts.js +346 -0
  115. package/dist/tools/primer-facts.test.js +353 -0
  116. package/dist/tools/project-listings.js +116 -0
  117. package/dist/tools/project-listings.test.js +106 -0
  118. package/dist/tools/query.js +65 -0
  119. package/dist/tools/query.test.js +128 -0
  120. package/dist/tools/raci.js +108 -0
  121. package/dist/tools/raci.test.js +70 -0
  122. package/dist/tools/requirements-spec.js +89 -0
  123. package/dist/tools/requirements-spec.test.js +62 -0
  124. package/dist/tools/response-expand.js +104 -0
  125. package/dist/tools/review-policy.js +105 -0
  126. package/dist/tools/review-policy.test.js +71 -0
  127. package/dist/tools/search.js +78 -0
  128. package/dist/tools/session-start.js +435 -0
  129. package/dist/tools/session-start.test.js +568 -0
  130. package/dist/tools/set-parent.js +76 -0
  131. package/dist/tools/set-parent.test.js +115 -0
  132. package/dist/tools/shared.js +226 -0
  133. package/dist/tools/shared.test.js +169 -0
  134. package/dist/tools/similar-projection.js +30 -0
  135. package/dist/tools/ticket-attachments.js +131 -0
  136. package/dist/tools/ticket-attachments.test.js +106 -0
  137. package/dist/tools/ticket-schedules.js +112 -0
  138. package/dist/tools/ticket-schedules.test.js +102 -0
  139. package/dist/tools/ticket-writes.js +778 -0
  140. package/dist/tools/ticket-writes.test.js +643 -0
  141. package/dist/tools/time-writes.js +238 -0
  142. package/dist/tools/time-writes.test.js +138 -0
  143. package/dist/tools/update-project.js +165 -0
  144. package/dist/tools/update-project.test.js +172 -0
  145. package/dist/tools/wiki.js +185 -0
  146. package/dist/tools/wiki.test.js +104 -0
  147. package/dist/tools/work-sessions-next.test.js +150 -0
  148. package/dist/tools/work-sessions-start.test.js +174 -0
  149. package/dist/tools/work-sessions.js +709 -0
  150. package/dist/tools/work-sessions.test.js +358 -0
  151. package/dist/toolset.js +149 -0
  152. package/dist/toolset.test.js +150 -0
  153. package/dist/version-guard.test.js +51 -0
  154. package/dist/version.js +28 -0
  155. package/dist/with-metrics-mail-nudge.test.js +41 -0
  156. package/dist/with-metrics-output-schema.test.js +91 -0
  157. package/dist/with-metrics.js +211 -0
  158. package/dist/with-metrics.test.js +230 -0
  159. package/package.json +66 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * ORB-510 / ORB-513 - primer-fact tools.
3
+ *
4
+ * Six tools that wrap the `/projects/:id/primer-facts` and
5
+ * `/primer-facts/:id` REST surface from ORB-511. Agents call these to
6
+ * record structured project facts - tech-stack details, conventions,
7
+ * deployment quirks - that the AI primer (ORB-512) renders at the top
8
+ * of every session. The skill rule (ORB-514) tells agents *when* to
9
+ * record; these tools are the *how*.
10
+ *
11
+ * All tool descriptions and parameter `.describe()` strings are in
12
+ * English so the international LLM tool-selection path is reliable.
13
+ *
14
+ * Wire order in server.ts:
15
+ * - orboto_primer_fact_list
16
+ * - orboto_primer_fact_add
17
+ * - orboto_primer_fact_update
18
+ * - orboto_primer_fact_supersede
19
+ * - orboto_primer_fact_verify
20
+ * - orboto_primer_fact_delete
21
+ */
22
+ import { z } from 'zod';
23
+ import { OrbotoApiError } from '../orboto-client.js';
24
+ import { resolveProjectByKey, sizeBlockResult } from './shared.js';
25
+ // ORB-1819 - the writing contract, verbatim in every write-tool
26
+ // description so the writing agent sees it at the moment of writing.
27
+ const FACT_WRITING_CONTRACT = ' Fact: <= 300 chars, one fact per key, the value is the fact - long material goes into a doc, the fact holds the doc key.';
28
+ // ---------------------------------------------------------------------------
29
+ // Shared types + helpers
30
+ // ---------------------------------------------------------------------------
31
+ const PRIMER_FACT_CATEGORIES = [
32
+ 'tech_stack',
33
+ 'conventions',
34
+ 'deployment',
35
+ 'architecture',
36
+ 'integrations',
37
+ 'gotchas',
38
+ 'commands',
39
+ // ORB-1403 - explicit non-functional requirements (performance, security,
40
+ // DSGVO, operations, i18n, ...) for the Pflichtenheft NFR chapter.
41
+ 'non_functional',
42
+ // ORB-1413 - glossary/terminology facts for the Pflichtenheft glossary
43
+ // chapter (chapter 11) only, so they no longer double-render via `other`.
44
+ 'glossary',
45
+ 'other',
46
+ ];
47
+ const PRIMER_FACT_SOURCES = ['manual', 'agent_observed', 'imported'];
48
+ const PrimerFactCategoryEnum = z.enum(PRIMER_FACT_CATEGORIES);
49
+ const PrimerFactSourceEnum = z.enum(PRIMER_FACT_SOURCES);
50
+ function rewritePermissionError(action) {
51
+ return (err) => {
52
+ if (err instanceof OrbotoApiError && err.status === 403) {
53
+ throw new Error(`${action} requires project:edit on the target project, or admin:ai:write for workspace-wide facts.`);
54
+ }
55
+ if (err instanceof OrbotoApiError && err.status === 404) {
56
+ throw new Error(`${action}: target not found (or not visible to your account).`);
57
+ }
58
+ if (err instanceof OrbotoApiError && err.status === 409) {
59
+ throw new Error(`${action}: a fact with that category + key already exists in this scope. Use orboto_primer_fact_supersede or orboto_primer_fact_update instead.`);
60
+ }
61
+ throw err;
62
+ };
63
+ }
64
+ function summariseFact(f) {
65
+ const markers = [];
66
+ if (f.source === 'agent_observed' && !f.verified)
67
+ markers.push('observed');
68
+ if (f.source === 'imported')
69
+ markers.push('imported');
70
+ if (f.supersededById)
71
+ markers.push('superseded');
72
+ const tag = markers.length > 0 ? ` _(${markers.join(', ')})_` : '';
73
+ // Single-line preview of the value - multi-line bodies get truncated
74
+ // so the list output stays scannable.
75
+ const valuePreview = f.value.length > 120 || f.value.includes('\n')
76
+ ? `${f.value.replace(/\n/g, ' ').slice(0, 120)}…`
77
+ : f.value;
78
+ const scope = f.projectId === null ? '[workspace]' : '[project]';
79
+ return `- ${scope} **${f.category}/${f.key}**: ${valuePreview}${tag}`;
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // orboto_primer_fact_list
83
+ // ---------------------------------------------------------------------------
84
+ export const primerFactListToolConfig = {
85
+ title: 'List structured project primer facts',
86
+ description: 'List structured project facts that feed the AI primer (tech stack, conventions, deployment, architecture, integrations, gotchas, commands). Use this to discover what the project has already documented before adding a new observation. Workspace-wide facts (applying to every project) are merged in by default; pass includeWorkspace=false to see only project-scoped rows.',
87
+ inputSchema: z.object({
88
+ projectKey: z.string().min(1).describe('Project key, e.g. "ORB". Case-insensitive.'),
89
+ category: PrimerFactCategoryEnum.optional().describe('Filter by category. Omit for all categories.'),
90
+ source: PrimerFactSourceEnum.optional().describe('Filter by provenance: manual (operator), agent_observed (bot, awaiting verification), imported.'),
91
+ verified: z.boolean().optional().describe('Filter by verification flag. true = verified facts only, false = unverified only.'),
92
+ includeWorkspace: z.boolean().default(true).describe('When true (default), workspace-wide facts that apply to every project are merged into the result.'),
93
+ }).shape,
94
+ annotations: { readOnlyHint: true, idempotentHint: true },
95
+ };
96
+ export function makePrimerFactListHandler(client) {
97
+ return async ({ projectKey, category, source, verified, includeWorkspace, }) => {
98
+ const project = await resolveProjectByKey(client, projectKey);
99
+ const qs = new URLSearchParams();
100
+ if (category)
101
+ qs.set('category', category);
102
+ if (source)
103
+ qs.set('source', source);
104
+ if (verified !== undefined)
105
+ qs.set('verified', String(verified));
106
+ qs.set('includeWorkspace', String(includeWorkspace ?? true));
107
+ const path = `/projects/${project.id}/primer-facts${qs.toString() ? `?${qs}` : ''}`;
108
+ const rows = await client.get(path).catch(rewritePermissionError('list primer facts'));
109
+ const text = rows.length === 0
110
+ ? 'No primer facts in scope.'
111
+ : rows.map(summariseFact).join('\n');
112
+ return {
113
+ content: [{ type: 'text', text }],
114
+ structuredContent: {
115
+ count: rows.length,
116
+ facts: rows.map((f) => ({
117
+ id: f.id,
118
+ scope: f.projectId === null ? 'workspace' : 'project',
119
+ category: f.category,
120
+ key: f.key,
121
+ value: f.value,
122
+ source: f.source,
123
+ verified: f.verified,
124
+ lastVerifiedAt: f.lastVerifiedAt,
125
+ })),
126
+ },
127
+ };
128
+ };
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // orboto_primer_fact_add
132
+ // ---------------------------------------------------------------------------
133
+ export const primerFactAddToolConfig = {
134
+ title: 'Record a new project primer fact',
135
+ description: 'Record a new structured fact about the project that future AI agents should see at session start. Use when you learn something during work that is missing from the primer (a library version, a convention, a deployment quirk, an architecture decision). Note: the auto-generated primer does NOT automatically include CLAUDE.md / AGENTS.md content unless the operator configured repo briefings and the API host can read those files from disk - most deployments cannot. Treat key conventions documented in CLAUDE.md / AGENTS.md as fair game to record here so they survive cross-deployment. Set observed=true when you are recording from a bot/agent context - the fact will land as agent_observed and require operator verification before losing the (observed) marker.'
136
+ + FACT_WRITING_CONTRACT,
137
+ inputSchema: z.object({
138
+ projectKey: z.string().min(1).describe('Project key, e.g. "ORB". Case-insensitive.'),
139
+ category: PrimerFactCategoryEnum.describe('Category bucket. Pick the closest fit; categories are fixed. Use "non_functional" to capture an explicit non-functional requirement (performance / security / DSGVO / operations / i18n) - the key names the sub-area, the value is the requirement; these render verbatim in the requirements-spec NFR chapter. Use "glossary" for a term/definition pair - these render only in the requirements-spec glossary chapter.'),
140
+ key: z.string().min(1).max(120).describe('Short machine-readable identifier, e.g. "package-manager", "node-version", "css-framework". Lowercase + dashes preferred.'),
141
+ value: z.string().min(1).max(8000).describe('Human-readable Markdown body, 1-3 sentences typically. Include a version number, a path, or a command where relevant.'),
142
+ observed: z.boolean().default(false).describe('True = source is agent_observed (needs operator verification). False = manual entry by operator. Bots without project:edit get coerced to agent_observed server-side regardless.'),
143
+ allowOversize: z.boolean().optional().describe('Override the hard size-cap block (300*2 chars). Only after a call was blocked.'),
144
+ oversizeReason: z.string().min(10).max(500).optional().describe('Required with allowOversize=true - why this value genuinely needs the length. Audit-logged.'),
145
+ }).shape,
146
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
147
+ };
148
+ export function makePrimerFactAddHandler(client) {
149
+ return async ({ projectKey, category, key, value, observed, allowOversize, oversizeReason, }) => {
150
+ const project = await resolveProjectByKey(client, projectKey);
151
+ const body = {
152
+ category,
153
+ key,
154
+ value,
155
+ source: observed ? 'agent_observed' : 'manual',
156
+ allowOversize,
157
+ oversizeReason,
158
+ };
159
+ let row;
160
+ try {
161
+ row = await client.post(`/projects/${project.id}/primer-facts`, body);
162
+ }
163
+ catch (err) {
164
+ const blocked = sizeBlockResult(err, 'Primer fact add');
165
+ if (blocked)
166
+ return blocked;
167
+ return rewritePermissionError('add primer fact')(err);
168
+ }
169
+ const warn = row.sizeWarning ? `\n⚠ ${row.value.length} chars, over the ${row.sizeWarning.limit}-char soft limit. ${row.sizeWarning.hint}` : '';
170
+ return {
171
+ content: [{
172
+ type: 'text',
173
+ text: `Recorded ${project.key} primer fact: ${row.category}/${row.key} (source: ${row.source}, id ${row.id.slice(0, 8)}).${warn}`,
174
+ }],
175
+ structuredContent: {
176
+ id: row.id,
177
+ category: row.category,
178
+ key: row.key,
179
+ source: row.source,
180
+ verified: row.verified,
181
+ },
182
+ };
183
+ };
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // orboto_primer_fact_update
187
+ // ---------------------------------------------------------------------------
188
+ export const primerFactUpdateToolConfig = {
189
+ title: 'Update an existing primer fact',
190
+ description: 'Update an existing fact. Use this when the value has changed (e.g. version bumped) or you are renaming the key. Bumps last_verified_at automatically when the value changes; pure renames leave it untouched. To replace a fact while preserving history, use orboto_primer_fact_supersede instead.'
191
+ + FACT_WRITING_CONTRACT,
192
+ inputSchema: z.object({
193
+ factId: z.string().min(1).describe('UUID of the fact to update. Read it from orboto_primer_fact_list output.'),
194
+ value: z.string().min(1).max(8000).optional().describe('New Markdown body.'),
195
+ category: PrimerFactCategoryEnum.optional().describe('New category.'),
196
+ key: z.string().min(1).max(120).optional().describe('New key.'),
197
+ allowOversize: z.boolean().optional().describe('Override the hard size-cap block (300*2 chars). Only after a call was blocked.'),
198
+ oversizeReason: z.string().min(10).max(500).optional().describe('Required with allowOversize=true - why this value genuinely needs the length. Audit-logged.'),
199
+ }).shape,
200
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
201
+ };
202
+ export function makePrimerFactUpdateHandler(client) {
203
+ return async ({ factId, value, category, key, allowOversize, oversizeReason, }) => {
204
+ const body = {};
205
+ if (value !== undefined)
206
+ body.value = value;
207
+ if (category !== undefined)
208
+ body.category = category;
209
+ if (key !== undefined)
210
+ body.key = key;
211
+ if (Object.keys(body).length === 0) {
212
+ throw new Error('orboto_primer_fact_update needs at least one of value, category, key.');
213
+ }
214
+ if (allowOversize !== undefined)
215
+ body.allowOversize = allowOversize;
216
+ if (oversizeReason !== undefined)
217
+ body.oversizeReason = oversizeReason;
218
+ let row;
219
+ try {
220
+ row = await client.patch(`/primer-facts/${factId}`, body);
221
+ }
222
+ catch (err) {
223
+ const blocked = sizeBlockResult(err, 'Primer fact update');
224
+ if (blocked)
225
+ return blocked;
226
+ return rewritePermissionError('update primer fact')(err);
227
+ }
228
+ const warn = row.sizeWarning ? `\n⚠ ${row.value.length} chars, over the ${row.sizeWarning.limit}-char soft limit. ${row.sizeWarning.hint}` : '';
229
+ return {
230
+ content: [{
231
+ type: 'text',
232
+ text: `Updated primer fact ${row.category}/${row.key} (id ${row.id.slice(0, 8)}).${warn}`,
233
+ }],
234
+ structuredContent: {
235
+ id: row.id,
236
+ category: row.category,
237
+ key: row.key,
238
+ lastVerifiedAt: row.lastVerifiedAt,
239
+ },
240
+ };
241
+ };
242
+ }
243
+ // ---------------------------------------------------------------------------
244
+ // orboto_primer_fact_supersede
245
+ // ---------------------------------------------------------------------------
246
+ export const primerFactSupersedeToolConfig = {
247
+ title: 'Replace a primer fact while preserving history',
248
+ description: 'Replace a fact with a corrected version, preserving the old one in history (linked via superseded_by_id). Use when you discover a previously recorded fact was wrong; do not just delete it. The old row gets a tombstoned key suffix so the partial unique index does not collide; the new row starts unverified and needs operator verification before losing the (observed) marker.'
249
+ + FACT_WRITING_CONTRACT,
250
+ inputSchema: z.object({
251
+ oldFactId: z.string().min(1).describe('UUID of the fact being replaced.'),
252
+ category: PrimerFactCategoryEnum.describe('Category of the new fact (usually unchanged).'),
253
+ key: z.string().min(1).max(120).describe('Key of the new fact (usually unchanged).'),
254
+ value: z.string().min(1).max(8000).describe('Corrected Markdown body.'),
255
+ allowOversize: z.boolean().optional().describe('Override the hard size-cap block (300*2 chars). Only after a call was blocked.'),
256
+ oversizeReason: z.string().min(10).max(500).optional().describe('Required with allowOversize=true - why this value genuinely needs the length. Audit-logged.'),
257
+ }).shape,
258
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
259
+ };
260
+ export function makePrimerFactSupersedeHandler(client) {
261
+ return async ({ oldFactId, category, key, value, allowOversize, oversizeReason, }) => {
262
+ let row;
263
+ try {
264
+ row = await client.post(`/primer-facts/${oldFactId}/supersede`, { category, key, value, allowOversize, oversizeReason });
265
+ }
266
+ catch (err) {
267
+ const blocked = sizeBlockResult(err, 'Primer fact supersede');
268
+ if (blocked)
269
+ return blocked;
270
+ return rewritePermissionError('supersede primer fact')(err);
271
+ }
272
+ const warn = row.sizeWarning ? `\n⚠ ${row.value.length} chars, over the ${row.sizeWarning.limit}-char soft limit. ${row.sizeWarning.hint}` : '';
273
+ return {
274
+ content: [{
275
+ type: 'text',
276
+ text: `Superseded primer fact: new id ${row.id.slice(0, 8)}, category ${row.category}, key ${row.key}.${warn}`,
277
+ }],
278
+ structuredContent: {
279
+ newId: row.id,
280
+ category: row.category,
281
+ key: row.key,
282
+ verified: row.verified,
283
+ },
284
+ };
285
+ };
286
+ }
287
+ // ---------------------------------------------------------------------------
288
+ // orboto_primer_fact_verify
289
+ // ---------------------------------------------------------------------------
290
+ export const primerFactVerifyToolConfig = {
291
+ title: 'Verify an agent-observed primer fact',
292
+ description: 'Promote an agent-observed fact to verified. Operator-only action; bots without project:edit cannot self-verify. Removes the (observed) marker from the rendered primer and stamps verifiedBy / verifiedAt.',
293
+ inputSchema: z.object({
294
+ factId: z.string().min(1).describe('UUID of the fact to verify.'),
295
+ }).shape,
296
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
297
+ };
298
+ export function makePrimerFactVerifyHandler(client) {
299
+ return async ({ factId }) => {
300
+ const row = await client
301
+ .post(`/primer-facts/${factId}/verify`, {})
302
+ .catch(rewritePermissionError('verify primer fact'));
303
+ return {
304
+ content: [{
305
+ type: 'text',
306
+ text: `Verified primer fact ${row.category}/${row.key} (id ${row.id.slice(0, 8)}).`,
307
+ }],
308
+ structuredContent: {
309
+ id: row.id,
310
+ verified: row.verified,
311
+ verifiedBy: row.verifiedBy,
312
+ verifiedAt: row.verifiedAt,
313
+ },
314
+ };
315
+ };
316
+ }
317
+ // ---------------------------------------------------------------------------
318
+ // orboto_primer_fact_delete
319
+ // ---------------------------------------------------------------------------
320
+ export const primerFactDeleteToolConfig = {
321
+ title: 'Delete a primer fact',
322
+ description: 'Hard-delete a fact. Prefer orboto_primer_fact_supersede when the fact is being replaced; only delete when the fact was wrong from the start or no longer relevant. Writes an audit-log entry; the row itself is gone (no soft-delete).',
323
+ inputSchema: z.object({
324
+ factId: z.string().min(1).describe('UUID of the fact to delete.'),
325
+ reason: z.string().max(500).optional().describe('Optional human-readable reason; recorded in the audit log details for future operators.'),
326
+ }).shape,
327
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
328
+ };
329
+ export function makePrimerFactDeleteHandler(client) {
330
+ return async ({ factId, reason }) => {
331
+ // ORB-516 - pass the reason through so the audit-log entry
332
+ // captures it. Old API versions silently ignore the querystring
333
+ // so there's no compat risk.
334
+ const path = reason
335
+ ? `/primer-facts/${factId}?reason=${encodeURIComponent(reason)}`
336
+ : `/primer-facts/${factId}`;
337
+ await client.delete(path).catch(rewritePermissionError('delete primer fact'));
338
+ return {
339
+ content: [{
340
+ type: 'text',
341
+ text: `Deleted primer fact ${factId.slice(0, 8)}${reason ? ` (reason: ${reason})` : ''}.`,
342
+ }],
343
+ structuredContent: { id: factId, deleted: true, reason: reason ?? null },
344
+ };
345
+ };
346
+ }