@adrata/adrata-mcp 1.0.3 → 1.0.7
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/README.md +29 -1
- package/access/auth.js +49 -1
- package/access/oauth.js +336 -23
- package/access/tiers.js +15 -0
- package/analytics.js +141 -20
- package/edge-block.js +118 -0
- package/package.json +3 -2
- package/server.js +130 -20
- package/server.json +14 -2
- package/skills/qa-the-card/SKILL.md +218 -0
- package/skills/ship-the-card/SKILL.md +18 -14
- package/tool-annotations.js +10 -2
- package/tools/source-control/connection-tools.js +31 -0
- package/tools/work-board-tools.js +1159 -48
- package/tools/work-hub/audit.js +17 -7
- package/toolsets/revenue/competitive-coverage.js +164 -0
- package/transport-http.js +30 -9
package/server.js
CHANGED
|
@@ -31,6 +31,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
31
31
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
32
32
|
import { z } from 'zod';
|
|
33
33
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
34
|
+
import { readFileSync } from 'node:fs';
|
|
34
35
|
import {
|
|
35
36
|
authenticate,
|
|
36
37
|
reauthenticate,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
import { TIERS } from './access/tiers.js';
|
|
42
43
|
import { findCompany, findPerson } from './tools/free-search.js';
|
|
43
44
|
import { applySecurityLayer } from './security.js';
|
|
45
|
+
import { describeEdgeBlock } from './edge-block.js';
|
|
44
46
|
import { registerMemoryTools, wrapWithEventLogging, registerProfileResource } from './tools/memory.js';
|
|
45
47
|
import { registerBillingTools } from './tools/billing.js';
|
|
46
48
|
import { registerMorningBrief } from './tools/morning-brief.js';
|
|
@@ -50,13 +52,15 @@ import { connectWorkspace, disconnectWorkspace, getConnectionStatus, getValidTok
|
|
|
50
52
|
import { registerEnterpriseTools } from './tools/enterprise-tools.js';
|
|
51
53
|
import { registerEmailTools } from './tools/email-tools.js';
|
|
52
54
|
import { registerWorkBoardTools } from './tools/work-board-tools.js';
|
|
55
|
+
import { registerSourceControlTools } from './tools/source-control/connection-tools.js';
|
|
53
56
|
import { registerRoadmapTools } from './tools/roadmap-tools.js';
|
|
54
57
|
import { registerPaperTools } from './tools/paper-tools.js';
|
|
55
58
|
import { register as registerAlwaysLoadedTools } from './toolsets/revenue/always-loaded.js';
|
|
56
59
|
import { register as registerExtensibilityTools } from './toolsets/extensibility.js';
|
|
57
60
|
import { registerSpaceTools } from './toolsets/spaces.js';
|
|
61
|
+
import { composeCompetitorIntel, composeCustomerSignals } from './toolsets/revenue/competitive-coverage.js';
|
|
58
62
|
import { getDemoAvailability, scheduleDemo, scheduleMeeting } from './tools/scheduling.js';
|
|
59
|
-
import { registerAnalytics } from './analytics.js';
|
|
63
|
+
import { registerAnalytics, wrapWithAnalytics } from './analytics.js';
|
|
60
64
|
import {
|
|
61
65
|
apiBridgeCatalog,
|
|
62
66
|
buildMutationHeaders,
|
|
@@ -131,7 +135,7 @@ async function api(method, path, { params, body, headers: extraHeaders } = {}) {
|
|
|
131
135
|
const auth = currentAuth();
|
|
132
136
|
assertOAuthIssuerMatchesTarget(auth, API_BASE);
|
|
133
137
|
let currentToken = auth.token;
|
|
134
|
-
if (auth.source === 'stored') {
|
|
138
|
+
if (auth.source === 'stored' || auth.source === 'stored_pool') {
|
|
135
139
|
let freshToken;
|
|
136
140
|
try {
|
|
137
141
|
freshToken = await getValidToken(API_BASE);
|
|
@@ -177,7 +181,7 @@ async function api(method, path, { params, body, headers: extraHeaders } = {}) {
|
|
|
177
181
|
};
|
|
178
182
|
|
|
179
183
|
let res = await request(currentToken);
|
|
180
|
-
if (res.status === 401 && auth.source === 'stored') {
|
|
184
|
+
if (res.status === 401 && (auth.source === 'stored' || auth.source === 'stored_pool')) {
|
|
181
185
|
let refreshedToken;
|
|
182
186
|
try {
|
|
183
187
|
refreshedToken = await getValidToken(API_BASE, { forceRefresh: true });
|
|
@@ -214,6 +218,18 @@ async function api(method, path, { params, body, headers: extraHeaders } = {}) {
|
|
|
214
218
|
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
215
219
|
|
|
216
220
|
if (!res.ok) {
|
|
221
|
+
// An HTML body cannot have come from the API — every AppError serialises as
|
|
222
|
+
// JSON — so it means the request was refused in front of us. Saying so is
|
|
223
|
+
// the whole point: a bare 403 is indistinguishable from insufficient_scope,
|
|
224
|
+
// and reading it as a token problem is what starts a needless reconnect.
|
|
225
|
+
const edge = describeEdgeBlock({
|
|
226
|
+
status: res.status,
|
|
227
|
+
text,
|
|
228
|
+
method,
|
|
229
|
+
path,
|
|
230
|
+
requestBody: body,
|
|
231
|
+
});
|
|
232
|
+
if (edge) throw new Error(edge);
|
|
217
233
|
throw new Error(`API ${method} ${path} → ${res.status}: ${JSON.stringify(data).slice(0, 200)}`);
|
|
218
234
|
}
|
|
219
235
|
return data;
|
|
@@ -297,8 +313,12 @@ const moneyWrite = {
|
|
|
297
313
|
* to start, which is the exact failure this whole surface keeps producing.
|
|
298
314
|
*/
|
|
299
315
|
const SERVER_NAME = process.env.ADRATA_MCP_SERVER_NAME?.trim() || '@adrata/adrata-mcp';
|
|
316
|
+
const PACKAGE_VERSION = JSON.parse(
|
|
317
|
+
readFileSync(new URL('./package.json', import.meta.url), 'utf8')
|
|
318
|
+
).version;
|
|
300
319
|
|
|
301
|
-
|
|
320
|
+
function createMcpServer() {
|
|
321
|
+
const server = new McpServer({ name: SERVER_NAME, version: PACKAGE_VERSION });
|
|
302
322
|
|
|
303
323
|
// ---------------------------------------------------------------------------
|
|
304
324
|
// Tier-gating wrapper
|
|
@@ -367,6 +387,26 @@ wrapWithEventLogging(server, api, AUTH);
|
|
|
367
387
|
// Must be applied AFTER tier-gating and event logging wrappers above.
|
|
368
388
|
applySecurityLayer(server, currentAuth, api);
|
|
369
389
|
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Analytics wrapper
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// MUST be installed here — after the wrappers above, and BEFORE the first
|
|
394
|
+
// server.tool(...) call below. It monkey-patches `server.tool`, so it can only
|
|
395
|
+
// instrument FUTURE registrations. It used to be installed from
|
|
396
|
+
// registerAnalytics() ~1975 lines further down, which put 227 of the 254 tools
|
|
397
|
+
// this server exposes ahead of it: trackToolInvocation and recordSessionTool
|
|
398
|
+
// could not fire for 89% of the surface, and get_mcp_analytics reported
|
|
399
|
+
// tool_invocations.total: 0 as though it were a measurement.
|
|
400
|
+
//
|
|
401
|
+
// The cost is deliberate and worth naming: every counted invocation now also
|
|
402
|
+
// fire-and-forget POSTs a `tool_invocation` event, roughly doubling the writes
|
|
403
|
+
// on /api/v1/mcp/events (the event-logging wrapper above already posts one per
|
|
404
|
+
// call for the whole surface). ADRATA_TELEMETRY=off disables both.
|
|
405
|
+
//
|
|
406
|
+
// registerAnalytics() still calls this; the wrapper is idempotent, so the later
|
|
407
|
+
// call is a no-op rather than a second layer.
|
|
408
|
+
wrapWithAnalytics(server, api, currentAuth, checkToolAccess);
|
|
409
|
+
|
|
370
410
|
// ===== WORKSPACE CONNECTION: OAuth 2.0 (available at all tiers) =====
|
|
371
411
|
|
|
372
412
|
server.tool('connect_workspace',
|
|
@@ -381,7 +421,18 @@ server.tool('connect_workspace',
|
|
|
381
421
|
AUTH = reauthenticate();
|
|
382
422
|
return ok(result);
|
|
383
423
|
} catch (err) {
|
|
384
|
-
|
|
424
|
+
// `invalid_scope` is not a browser problem, and saying it is costs real
|
|
425
|
+
// time: it means the authorization server rejected one of the scopes we
|
|
426
|
+
// asked for, which in practice means the deployed API is running behind
|
|
427
|
+
// this client. The whole request is refused, so read-only will still
|
|
428
|
+
// connect — that asymmetry is the tell.
|
|
429
|
+
const hint = /invalid_scope/.test(err.message ?? '')
|
|
430
|
+
? 'The API rejected a requested scope, so it granted nothing — this is NOT a browser problem. '
|
|
431
|
+
+ 'The deployed API is probably behind this client. Compare them: '
|
|
432
|
+
+ 'curl -s ' + API_BASE + '/health | jq -r .build_sha '
|
|
433
|
+
+ 'Read-only connect_workspace() should still work; use it to keep going.'
|
|
434
|
+
: 'Make sure your browser is accessible and try again.';
|
|
435
|
+
return ok({ error: true, message: err.message, hint });
|
|
385
436
|
}
|
|
386
437
|
});
|
|
387
438
|
|
|
@@ -412,7 +463,7 @@ server.tool('workspace_status',
|
|
|
412
463
|
// answer from a diagnostic: it sends someone to reconnect a connection that
|
|
413
464
|
// was never the problem, and it is the tool a connect flow tells you to run
|
|
414
465
|
// to confirm the setup worked.
|
|
415
|
-
const usingStoredSession = AUTH.source === 'stored';
|
|
466
|
+
const usingStoredSession = AUTH.source === 'stored' || AUTH.source === 'stored_pool';
|
|
416
467
|
const stored = await getConnectionStatus(API_BASE);
|
|
417
468
|
|
|
418
469
|
// AUTH.workspaceId is only populated when the workspace was pinned via env
|
|
@@ -452,8 +503,11 @@ server.tool('workspace_status',
|
|
|
452
503
|
return ok({
|
|
453
504
|
...stored,
|
|
454
505
|
tier: stored.connected ? AUTH.tier : TIERS.FREE,
|
|
455
|
-
credential:
|
|
506
|
+
credential: AUTH.source === 'stored_pool'
|
|
507
|
+
? `dedicated OAuth session for named identity pool ${AUTH.identityPool}`
|
|
508
|
+
: 'stored OAuth session (~/.adrata/tokens.json)',
|
|
456
509
|
authSource: AUTH.source,
|
|
510
|
+
identityPool: AUTH.identityPool || null,
|
|
457
511
|
apiBase: API_BASE,
|
|
458
512
|
workspaceId,
|
|
459
513
|
workspaceName,
|
|
@@ -521,7 +575,7 @@ function assertProductCapability(capability, requested) {
|
|
|
521
575
|
}
|
|
522
576
|
|
|
523
577
|
async function resolveExactCapability(reference) {
|
|
524
|
-
const response = await api('GET', '/api/v1/ai
|
|
578
|
+
const response = await api('GET', '/api/v1/ai-crm-tools/capabilities/describe', {
|
|
525
579
|
params: { ref: reference },
|
|
526
580
|
});
|
|
527
581
|
return assertProductCapability(response?.capability ?? response?.data?.capability, reference);
|
|
@@ -539,7 +593,7 @@ server.tool('search_capabilities',
|
|
|
539
593
|
if (PRODUCT_NAMESPACE && requestedNamespace && requestedNamespace !== PRODUCT_NAMESPACE) {
|
|
540
594
|
throw new Error(`${SERVER_NAME} search is fixed to the ${PRODUCT_NAMESPACE} namespace.`);
|
|
541
595
|
}
|
|
542
|
-
return ok(await api('GET', '/api/v1/ai
|
|
596
|
+
return ok(await api('GET', '/api/v1/ai-crm-tools/capabilities/search', {
|
|
543
597
|
params: {
|
|
544
598
|
q: args.query,
|
|
545
599
|
namespace: PRODUCT_NAMESPACE || requestedNamespace,
|
|
@@ -581,7 +635,7 @@ server.tool('run_capability',
|
|
|
581
635
|
if (args.idempotencyKey) headers['idempotency-key'] = args.idempotencyKey;
|
|
582
636
|
if (args.reason) headers['x-adrata-reason'] = args.reason;
|
|
583
637
|
if (args.confirmSpend === true) headers['x-adrata-approved'] = 'true';
|
|
584
|
-
return ok(await api('POST', '/api/v1/ai
|
|
638
|
+
return ok(await api('POST', '/api/v1/ai-crm-tools/execute', { body, headers }));
|
|
585
639
|
});
|
|
586
640
|
|
|
587
641
|
server.tool('adrata_api_catalog',
|
|
@@ -1756,7 +1810,7 @@ server.tool('get_intent_signals', 'Get buying intent signals for a company.',
|
|
|
1756
1810
|
// are workspace-scoped from the token; filter by subject (a company or person),
|
|
1757
1811
|
// source, signal type, or minimum score, and page with the returned cursor.
|
|
1758
1812
|
server.tool('list_customer_signals',
|
|
1759
|
-
'List first-party customer signals (the store live plays fire from) for the workspace, or scope to one company/person via subjectType + subjectId. Distinct from get_intent_signals (third-party intent).',
|
|
1813
|
+
'List first-party customer signals (the store live plays fire from) for the workspace, or scope to one company/person via subjectType + subjectId. Distinct from get_intent_signals (third-party intent). Always relay the returned `coverage`: an empty result is checked against the unfiltered workspace, so a real zero is distinguished from a store nothing has ever written to.',
|
|
1760
1814
|
{
|
|
1761
1815
|
subjectType: z.enum(['company', 'person']).optional().describe('Scope to a subject kind; pair with subjectId'),
|
|
1762
1816
|
subjectId: z.string().optional().describe('Company or person ID to scope signals to'),
|
|
@@ -1768,11 +1822,31 @@ server.tool('list_customer_signals',
|
|
|
1768
1822
|
limit: z.number().optional().describe('Page size (default 50, max 500)'),
|
|
1769
1823
|
cursor: z.string().optional().describe('Opaque cursor from a previous page'),
|
|
1770
1824
|
},
|
|
1771
|
-
async (a) =>
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1825
|
+
async (a) => {
|
|
1826
|
+
const filtersApplied = {
|
|
1827
|
+
subjectType: a.subjectType, subjectId: a.subjectId, source: a.source,
|
|
1828
|
+
signalType: a.signalType, minScore: a.minScore, since: a.since, until: a.until,
|
|
1829
|
+
};
|
|
1830
|
+
const payload = await api('GET', '/api/v1/customer-signals', { params: {
|
|
1831
|
+
...filtersApplied, limit: a.limit || 50, cursor: a.cursor,
|
|
1832
|
+
} });
|
|
1833
|
+
|
|
1834
|
+
// The probe is spent ONLY on the ambiguous case. Rows returned means the store is
|
|
1835
|
+
// demonstrably written to and the count speaks for itself; an empty array is the reading a
|
|
1836
|
+
// seller cannot interpret, and one extra request is what separates "no signals match these
|
|
1837
|
+
// filters" from "nothing has ever been ingested".
|
|
1838
|
+
const empty = Array.isArray(payload?.items) ? payload.items.length === 0 : undefined;
|
|
1839
|
+
let workspaceProbePayload;
|
|
1840
|
+
if (empty === true) {
|
|
1841
|
+
try {
|
|
1842
|
+
workspaceProbePayload = await api('GET', '/api/v1/customer-signals', { params: { limit: 1 } });
|
|
1843
|
+
} catch {
|
|
1844
|
+
workspaceProbePayload = null;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return ok(composeCustomerSignals({ payload, filtersApplied, workspaceProbePayload }));
|
|
1849
|
+
});
|
|
1776
1850
|
|
|
1777
1851
|
server.tool('get_deal_authority', 'Get authority mapping and stakeholder analysis for a deal.',
|
|
1778
1852
|
{ opportunityId: z.string() },
|
|
@@ -1781,9 +1855,39 @@ server.tool('get_deal_authority', 'Get authority mapping and stakeholder analysi
|
|
|
1781
1855
|
params: { opportunityId: a.opportunityId },
|
|
1782
1856
|
})));
|
|
1783
1857
|
|
|
1784
|
-
server.tool('get_competitor_intel',
|
|
1858
|
+
server.tool('get_competitor_intel',
|
|
1859
|
+
'Competitive intelligence for ONE company: the competitor mentions attributed to that account, plus the workspace-wide competitor definitions they are scored against. Always relay the returned `coverage` — it says which half is company-scoped and which is workspace-wide, and states whether a zero is a real zero or an unconfigured store. `/api/v1/competitors` is a workspace configuration list and takes no company filter; the company-scoped evidence lives at `/api/v1/competitors/mentions?companyId=`.',
|
|
1785
1860
|
{ companyId: z.string() },
|
|
1786
|
-
async (a) =>
|
|
1861
|
+
async (a) => {
|
|
1862
|
+
// The definition list is workspace-scoped by construction. Sending `companyId` to it — as
|
|
1863
|
+
// this tool used to — got the parameter silently discarded and returned the same answer for
|
|
1864
|
+
// every company. The two reads are separate because their SCOPES are separate.
|
|
1865
|
+
const definitionsPayload = await api('GET', '/api/v1/competitors');
|
|
1866
|
+
|
|
1867
|
+
let mentionsPayload;
|
|
1868
|
+
let workspaceMentionsPayload;
|
|
1869
|
+
let mentionsUnavailable = false;
|
|
1870
|
+
try {
|
|
1871
|
+
mentionsPayload = await api('GET', '/api/v1/competitors/mentions', {
|
|
1872
|
+
params: { companyId: a.companyId, limit: 50 },
|
|
1873
|
+
});
|
|
1874
|
+
// The denominator. One extra cheap request buys the difference between "this account is
|
|
1875
|
+
// quiet" and "nothing has ever been scanned".
|
|
1876
|
+
workspaceMentionsPayload = await api('GET', '/api/v1/competitors/mentions', {
|
|
1877
|
+
params: { limit: 1 },
|
|
1878
|
+
});
|
|
1879
|
+
} catch {
|
|
1880
|
+
mentionsUnavailable = true;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
return ok(composeCompetitorIntel({
|
|
1884
|
+
companyId: a.companyId,
|
|
1885
|
+
definitionsPayload,
|
|
1886
|
+
mentionsPayload,
|
|
1887
|
+
workspaceMentionsPayload,
|
|
1888
|
+
mentionsUnavailable,
|
|
1889
|
+
}));
|
|
1890
|
+
});
|
|
1787
1891
|
|
|
1788
1892
|
// ===== ANALYTICS =====
|
|
1789
1893
|
|
|
@@ -2266,6 +2370,8 @@ registerWorkBoardTools(server, {
|
|
|
2266
2370
|
getGrantedScope: () => loadTokens()?.scope,
|
|
2267
2371
|
});
|
|
2268
2372
|
|
|
2373
|
+
registerSourceControlTools(server, { z, api, ok });
|
|
2374
|
+
|
|
2269
2375
|
// The containers above the cards, and the "add this to the roadmap" verb
|
|
2270
2376
|
// (company/decisions/2026-08-06-spoq-roadmap-sync.md).
|
|
2271
2377
|
registerRoadmapTools(server, {
|
|
@@ -2323,7 +2429,7 @@ server.tool('schedule_meeting',
|
|
|
2323
2429
|
// Tracks tool invocations, search queries, tier gates, conversions, sessions.
|
|
2324
2430
|
// Builds on top of existing event logging — adds classification and dashboards.
|
|
2325
2431
|
// ADRATA_TELEMETRY=off disables all tracking. PII is hashed before storage.
|
|
2326
|
-
registerAnalytics(server, { z, api, auth:
|
|
2432
|
+
registerAnalytics(server, { z, api, auth: currentAuth, ok, checkToolAccess });
|
|
2327
2433
|
|
|
2328
2434
|
// ===== COMPOSITE TOOLSETS =====
|
|
2329
2435
|
// 42 composite tools organized into 7 toolsets with dynamic discovery.
|
|
@@ -2344,6 +2450,9 @@ registerExtensibilityTools(server, api, AUTH);
|
|
|
2344
2450
|
// Service" got nothing, while the app answered it fine.
|
|
2345
2451
|
registerSpaceTools(server, { z, api, ok });
|
|
2346
2452
|
|
|
2453
|
+
return server;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2347
2456
|
// ---------------------------------------------------------------------------
|
|
2348
2457
|
// Start
|
|
2349
2458
|
// ---------------------------------------------------------------------------
|
|
@@ -2353,11 +2462,12 @@ const TRANSPORT_MODE = (process.env.ADRATA_MCP_TRANSPORT || 'stdio').toLowerCase
|
|
|
2353
2462
|
if (TRANSPORT_MODE === 'http' || TRANSPORT_MODE === 'sse') {
|
|
2354
2463
|
// Streamable HTTP transport — single POST /mcp endpoint that can upgrade to SSE
|
|
2355
2464
|
const { startHttpTransport } = await import('./transport-http.js');
|
|
2356
|
-
await startHttpTransport(
|
|
2465
|
+
await startHttpTransport(createMcpServer, {
|
|
2357
2466
|
runWithAuthContext,
|
|
2358
2467
|
});
|
|
2359
2468
|
} else {
|
|
2360
2469
|
// Default: stdio transport (backward compatible)
|
|
2470
|
+
const server = createMcpServer();
|
|
2361
2471
|
const transport = new StdioServerTransport();
|
|
2362
2472
|
await server.connect(transport);
|
|
2363
2473
|
}
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "com.adrata/adrata-mcp",
|
|
4
4
|
"description": "Adrata revenue-intelligence MCP server: companies, people, opportunities, actions, buyer groups, enrichment, email, and workspace operations for AI agents.",
|
|
5
5
|
"status": "active",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.7",
|
|
7
7
|
"websiteUrl": "https://adrata.com/developers",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/adrata/adrata",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
17
|
"identifier": "@adrata/adrata-mcp",
|
|
18
|
-
"version": "1.0.
|
|
18
|
+
"version": "1.0.7",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
},
|
|
@@ -31,6 +31,18 @@
|
|
|
31
31
|
"description": "Enterprise OAuth 2.1 access token, audience-bound to the Adrata MCP resource (RFC 8707).",
|
|
32
32
|
"isRequired": false,
|
|
33
33
|
"isSecret": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "ADRATA_MCP_IDENTITY_POOL",
|
|
37
|
+
"description": "Optional named worker identity (for example qa1 or qa2). Named pools fail closed and only use their dedicated stored OAuth session.",
|
|
38
|
+
"isRequired": false,
|
|
39
|
+
"isSecret": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "ADRATA_MCP_CONFIG_DIR",
|
|
43
|
+
"description": "OAuth storage directory. Required and non-default when ADRATA_MCP_IDENTITY_POOL is set.",
|
|
44
|
+
"isRequired": false,
|
|
45
|
+
"isSecret": false
|
|
34
46
|
}
|
|
35
47
|
]
|
|
36
48
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-the-card
|
|
3
|
+
description: Deeply verify a Starfield card in Staging QA1 or Staging QA2, fix defects and rerun evidence until clean, then leave an auditable AI-to-human handoff without claiming deployment. Use when asked to QA, review, verify, sign off, or clear cards from a QA column.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# QA the card
|
|
7
|
+
|
|
8
|
+
QA is an engineering pass, not a documentation pass. Reproduce every acceptance
|
|
9
|
+
claim, attack the implementation beyond the happy path, fix defects in scope,
|
|
10
|
+
and repeat until the card is genuinely ready for the next accountable reviewer.
|
|
11
|
+
|
|
12
|
+
The workflow semantics are fixed by
|
|
13
|
+
`company/decisions/2026-08-29-two-gate-ai-qa-human-production.md`; the required
|
|
14
|
+
evidence loop is fixed by
|
|
15
|
+
`company/decisions/2026-08-29-recorded-staging-qa-standard.md`. This skill is
|
|
16
|
+
their operating procedure.
|
|
17
|
+
|
|
18
|
+
## Establish the real queue
|
|
19
|
+
|
|
20
|
+
1. Read every active board or the all-boards roll-up before announcing a count.
|
|
21
|
+
A card's `product` may differ from the board holding it, so inspect both the
|
|
22
|
+
Adrata board and Adrata-labelled cards on another board. Re-read the queue
|
|
23
|
+
during a long session and at every deployment checkpoint because new cards
|
|
24
|
+
can arrive while QA is running. Report the delta, not only the new total.
|
|
25
|
+
2. Limit mutations to the QA scope the user named. Read each card, its full
|
|
26
|
+
stage history, comments, acceptance criteria, flag, linked pull request, and
|
|
27
|
+
exact-SHA delivery evidence before testing it.
|
|
28
|
+
3. Do not infer missing history from a sparse screen. Confirm the transition
|
|
29
|
+
rows through the governed API. A card created directly in an old QA column
|
|
30
|
+
may honestly have only one transition.
|
|
31
|
+
4. Audit the criteria themselves. Each must describe an observable outcome and
|
|
32
|
+
be executable at the current gate. Add or correct missing coverage before
|
|
33
|
+
calling the card clean; never turn a vague criterion green by interpreting it
|
|
34
|
+
generously.
|
|
35
|
+
5. Missing independent QA evidence by itself is expected at QA1 entry and never
|
|
36
|
+
justifies a bounce. Do not report a vague “missing evidence path”: name the
|
|
37
|
+
allegedly missing product route, fixture, credential, artifact location, or
|
|
38
|
+
current-dwell QA receipt, and resolve relative artifact paths against the
|
|
39
|
+
owning worktree or durable location before declaring them absent. If the
|
|
40
|
+
executable prerequisite truly is missing, keep the card in QA and run the
|
|
41
|
+
same-card fix-and-retest loop; if only the independent receipt is missing,
|
|
42
|
+
start the pass.
|
|
43
|
+
|
|
44
|
+
## Preserve independent gates
|
|
45
|
+
|
|
46
|
+
- QA1 and QA2 are separate passes over separate column-history dwells. A prior
|
|
47
|
+
checkbox or receipt is evidence to inspect, not a substitute for rerunning it.
|
|
48
|
+
- An agent's QA2 credential must differ from the criterion author and the agent
|
|
49
|
+
credential whose clean receipt opened QA1. Never impersonate a human to bypass
|
|
50
|
+
this rule. A human may perform the final acceptance even when they own or
|
|
51
|
+
authored the card.
|
|
52
|
+
- **Take that credential from a QA lane, and take a different one per
|
|
53
|
+
worktree.** Staging seeds twenty-four of them — `qa-lane-1@adrata.test`
|
|
54
|
+
through `qa-lane-24@adrata.test`, lanes 1-18 `admin` and 19-24 `seller`.
|
|
55
|
+
There are deliberately more lanes than the fleet can drive, so a tester
|
|
56
|
+
never queues behind an identity:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
node scripts/qa-staging-lane.mjs --list # every lane and its role
|
|
60
|
+
node scripts/qa-staging-lane.mjs <lane> --write-env # this worktree's tests/e2e/.env
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The rule above is unsatisfiable with one shared login, and the failure is
|
|
64
|
+
silent rather than loud: every recording is the same actor, so cards get
|
|
65
|
+
honestly CLAIMED and none can be legitimately VERIFIED. Sharing a lane
|
|
66
|
+
between two live agents is the same defect wearing a different hat — they
|
|
67
|
+
share a session, a workspace switch and a chat history, so one lane's
|
|
68
|
+
navigation lands in another lane's recording. Record the lane on the receipt,
|
|
69
|
+
so "a different credential" is a checkable claim rather than an assertion.
|
|
70
|
+
The accounts come from the non-production boot seed
|
|
71
|
+
(`code/api/crates/schema/src/seeds/non_prod.rs`) and exist only on staging
|
|
72
|
+
and local; they cannot be created in production.
|
|
73
|
+
- For non-trivial QA2 work, use at least one independent adversarial reviewer
|
|
74
|
+
when subagents are authorized and available. Parallelize separable surfaces
|
|
75
|
+
such as UI, backend/security, and migration/deployment behavior. Give each
|
|
76
|
+
reviewer a bounded target and avoid concurrent edits to the same files.
|
|
77
|
+
- Multiple reviewers may find and challenge defects, but they do not assemble a
|
|
78
|
+
synthetic gate from partial identities. One eligible second-agent credential
|
|
79
|
+
must rerun or confirm every current criterion so the final QA2 receipt is
|
|
80
|
+
coherent and the badge can be derived honestly.
|
|
81
|
+
- Stagger expensive Rust builds on the shared Mac. More concurrent compilers can
|
|
82
|
+
make verification slower and can exhaust disk; an independent final result
|
|
83
|
+
must use the reviewer's own target artifacts and show any new test by name.
|
|
84
|
+
|
|
85
|
+
## Run the fix-and-retest loop
|
|
86
|
+
|
|
87
|
+
For every criterion:
|
|
88
|
+
|
|
89
|
+
1. Write down the observable, the environment, and the strongest practical way
|
|
90
|
+
to falsify it. Source inspection alone is not a pass.
|
|
91
|
+
2. Reproduce the behavior before changing code when the deployed failure is
|
|
92
|
+
observable. Preserve the failing request, console output, screenshot, video,
|
|
93
|
+
trace, or exact database/API result.
|
|
94
|
+
3. Exercise the happy path, boundary conditions, failure states, authorization
|
|
95
|
+
boundaries, retries/idempotency, and a realistic regression path. For a
|
|
96
|
+
migration, prove both representative old state and transactional rerun. For
|
|
97
|
+
a deployment, prove the exact deployed SHA and behavior during replacement,
|
|
98
|
+
not only the final steady state.
|
|
99
|
+
4. If anything fails, fix it within the same card when passing the card's
|
|
100
|
+
existing acceptance outcome necessarily requires that fix. Add the narrowest
|
|
101
|
+
regression test that would have caught it, rerun the focused suite, then rerun
|
|
102
|
+
the broader affected contracts. Do not merely describe a defect and stop
|
|
103
|
+
while a safe in-scope fix remains.
|
|
104
|
+
5. Repeat adversarial review after the fix. A first green rerun is evidence, not
|
|
105
|
+
automatic sign-off.
|
|
106
|
+
|
|
107
|
+
Keep the card in its QA column throughout this loop unless the user or board
|
|
108
|
+
policy explicitly asks for a bounce. Never hide an incomplete pass by moving it
|
|
109
|
+
forward.
|
|
110
|
+
|
|
111
|
+
## Visual QA requires visual evidence
|
|
112
|
+
|
|
113
|
+
Use Playwright for every user-facing change. A DOM assertion or source review
|
|
114
|
+
does not establish that the screen is visually correct.
|
|
115
|
+
|
|
116
|
+
- Record a WebM video of the real interaction and take screenshots of the states
|
|
117
|
+
that decide the criterion. Enable action annotations or chapter markers when
|
|
118
|
+
they make the recording easier for the human reviewer to follow.
|
|
119
|
+
- Measure duration, MIME type, size, and hash from the finalized media file
|
|
120
|
+
before upload. Never declare the interaction stopwatch as the video duration:
|
|
121
|
+
recorder startup/finalization can make those values differ. Reject the
|
|
122
|
+
receipt when a checkpoint falls outside the measured media duration or the
|
|
123
|
+
declared metadata does not match the finalized file.
|
|
124
|
+
- In QA2, inspect the QA1 recording as prior evidence, then record a new QA2
|
|
125
|
+
replay. Keep both attached to their exact gate and column-history dwell. Never
|
|
126
|
+
reuse, replace, or relabel the QA1 media as QA2 evidence.
|
|
127
|
+
- Give each concurrent QA agent an isolated synthetic account or an explicit
|
|
128
|
+
exclusive lease on the shared account. For stateful journeys, create and read
|
|
129
|
+
back a unique server resource (for example, a conversation id) and address it
|
|
130
|
+
directly; never rely on the newest active resource. An unexpected request,
|
|
131
|
+
turn, or mutation from outside the recorded browser makes the run ambiguous
|
|
132
|
+
and therefore a failure, not evidence.
|
|
133
|
+
- Inspect the rendered screenshots yourself at the target viewport. Check
|
|
134
|
+
alignment, clipping, wrapping, whitespace, stacking, focus, disabled/loading
|
|
135
|
+
behavior, and whether badges and controls line up with neighboring content.
|
|
136
|
+
- Capture the browser console, page errors, failed requests, and the relevant
|
|
137
|
+
network responses. Distinguish an expected simulated failure from an
|
|
138
|
+
unexpected product error.
|
|
139
|
+
- Cover normal, empty, loading, error, and recovery states when they exist. Test
|
|
140
|
+
both hosted Starfield and the packaged desktop app when both ship the changed
|
|
141
|
+
renderer.
|
|
142
|
+
- When governed QA evidence is available, attach the screenshot/video to the
|
|
143
|
+
card's exact open QA dwell with the environment and build SHA. Until that
|
|
144
|
+
surface is deployed, retain the files and record their paths or durable run
|
|
145
|
+
links without pretending they were attached.
|
|
146
|
+
- Reopen both uploaded gate recordings before handoff. Prove each fresh
|
|
147
|
+
short-lived URL loads and that each video can play, seek, and enter
|
|
148
|
+
fullscreen; an upload response alone is not a media pass.
|
|
149
|
+
- A staging QA lane and the canonical board may intentionally use separate data
|
|
150
|
+
planes. Do not copy a production card into staging, reuse the builder's browser
|
|
151
|
+
credential, or put an OAuth bearer or presigned media URL in the transcript.
|
|
152
|
+
Run `npm run --silent qa:canonical-card-review -- <card-id>` under the fresh
|
|
153
|
+
reviewer's named `ADRATA_MCP_IDENTITY_POOL` and dedicated
|
|
154
|
+
`ADRATA_MCP_CONFIG_DIR`. It opens the exact deployed product bundle through a
|
|
155
|
+
one-use, read-only loopback session: upstream reads keep the QA OAuth identity,
|
|
156
|
+
writes fail locally, and private media capabilities remain process-private.
|
|
157
|
+
Record the returned session fingerprint and the deployed app build in the
|
|
158
|
+
playback evidence, then close the bridge.
|
|
159
|
+
|
|
160
|
+
## Manage cards as outcomes, not bug counters
|
|
161
|
+
|
|
162
|
+
- A defect required to satisfy the current card stays on that card. Fix it and
|
|
163
|
+
retest there; do not create a new Up Next card merely because QA found it.
|
|
164
|
+
- Create a separate card only for an independently releasable outcome outside
|
|
165
|
+
the current acceptance scope. Search the whole board first. If a likely
|
|
166
|
+
duplicate already exists, identify the pair and let a human collapse it; do
|
|
167
|
+
not delete or merge cards on your own judgement.
|
|
168
|
+
- A new card needs a concrete title, reproduction/context, first-class
|
|
169
|
+
acceptance criteria, and no invented assignee. Move it through the ordinary
|
|
170
|
+
workflow instead of placing it directly in a protected QA or done column.
|
|
171
|
+
- Keep findings, fixes, and evidence on the canonical card. Do not rewrite the
|
|
172
|
+
original body to make the request match what was discovered.
|
|
173
|
+
|
|
174
|
+
## Promotion and the human handoff
|
|
175
|
+
|
|
176
|
+
Local verification comes before CI. Keep incomplete pull requests draft, seek
|
|
177
|
+
an independent code review for consequential fixes, make the PR ready only
|
|
178
|
+
after local evidence is clean, and merge only after the applicable protected
|
|
179
|
+
checks pass. Verify the cumulative staging deployment rather than treating a
|
|
180
|
+
merge as deployment proof.
|
|
181
|
+
|
|
182
|
+
When QA2 is clean:
|
|
183
|
+
|
|
184
|
+
1. Tick only criteria actually executed in the current dwell, with concise
|
|
185
|
+
evidence naming commands or run links, exact counts, environment, build SHA,
|
|
186
|
+
and any material caveat.
|
|
187
|
+
2. Add one concise final card comment summarizing the fixes and the last clean
|
|
188
|
+
rerun. Comments preserve the audit trail; they never replace the fix.
|
|
189
|
+
3. Confirm the derived `Ready for review` badge appears and aligns correctly in
|
|
190
|
+
both the card and detail view. Never edit an emoji into the title; a derived
|
|
191
|
+
badge disappears correctly after a bounce.
|
|
192
|
+
4. Leave the card in Staging QA2. An agent must not move it to Ready to Ship or
|
|
193
|
+
Production. Ready to Ship is the human's acceptance of both AI receipts.
|
|
194
|
+
5. Treat Production as a separate claim: the approved release is live, its exact
|
|
195
|
+
delivery evidence is known, and release notes may now include it. A board
|
|
196
|
+
column alone is not exact-SHA proof.
|
|
197
|
+
|
|
198
|
+
If the badge does not appear, identity is ambiguous, evidence belongs to an old
|
|
199
|
+
dwell, a criterion is open, CI is incomplete, or the deployed behavior differs,
|
|
200
|
+
the card is not ready. Fix the cause and rerun; never use an override to make the
|
|
201
|
+
handoff look complete.
|
|
202
|
+
|
|
203
|
+
## Report while working
|
|
204
|
+
|
|
205
|
+
Keep the human informed with concrete state: current QA2 count, cards clean,
|
|
206
|
+
cards in fix/retest, newly found defects, and deployment/desktop readiness. Do
|
|
207
|
+
not announce that the app is ready to inspect until the required API migration,
|
|
208
|
+
hosted build, packaged desktop version, relaunch, and final Playwright video pass
|
|
209
|
+
are actually complete.
|
|
210
|
+
|
|
211
|
+
At handoff report:
|
|
212
|
+
|
|
213
|
+
- card and current column;
|
|
214
|
+
- criteria executed and the final evidence;
|
|
215
|
+
- defects found, fixes made, and independent review result;
|
|
216
|
+
- exact staging/production/desktop build state;
|
|
217
|
+
- whether the `Ready for review` badge is visible;
|
|
218
|
+
- any genuinely separate follow-up card.
|
|
@@ -9,6 +9,10 @@ This is the loop a coding agent actually runs: read a card, do the work, move
|
|
|
9
9
|
the card. The part people get wrong is the last step — work that lands with the
|
|
10
10
|
board untouched is work nobody can see.
|
|
11
11
|
|
|
12
|
+
If the card is already in Staging QA1 or Staging QA2, use `qa-the-card` instead.
|
|
13
|
+
QA is a fresh independent verification and fix-and-retest loop, not the ordinary
|
|
14
|
+
implementation-and-submit path below.
|
|
15
|
+
|
|
12
16
|
## Pick up
|
|
13
17
|
|
|
14
18
|
1. `list_my_work_items` — **start here.** It answers "what is mine", across every
|
|
@@ -118,15 +122,15 @@ number, a run link, the caveat that makes it honest. An untouched checkbox says
|
|
|
118
122
|
nothing about whether the check passed; it says nobody has been near it, which
|
|
119
123
|
is the same thing an abandoned card says.
|
|
120
124
|
|
|
121
|
-
Ticking from a build column records `claimed`, not `verified
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
the
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
Ticking from a build column records `claimed`, not `verified`. At a QA gate, an
|
|
126
|
+
authenticated human may verify even when they own the card or authored the
|
|
127
|
+
criterion: the human is the accountable exception when one person operates the
|
|
128
|
+
workspace. An agent verifies only when its credential differs from the
|
|
129
|
+
criterion author, and QA2 requires a different agent credential from the one
|
|
130
|
+
whose clean receipt opened QA1. One AI cannot certify both gates. Tick anyway
|
|
131
|
+
when the result is `claimed` — it is a real state that says the work is believed
|
|
132
|
+
done and tells QA what to rerun. Never tick a box you did not execute merely to
|
|
133
|
+
improve the count.
|
|
130
134
|
|
|
131
135
|
If a tick turns out to be wrong, `unsatisfy_work_item_acceptance_criterion`
|
|
132
136
|
takes it off. It clears the note with it, so if the evidence is worth keeping,
|
|
@@ -151,11 +155,11 @@ The tool refuses placeholder reasons — "moved", "update", "Moved to another
|
|
|
151
155
|
column" — on purpose: a history full of those looks like an audit trail, so
|
|
152
156
|
nobody goes looking for the real answer, which is worse than no history at all.
|
|
153
157
|
|
|
154
|
-
**Do not move a card into
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
**Do not move a card into `Ready to Ship` or `Production`.** Ready to Ship is the
|
|
159
|
+
human's final acceptance of both AI QA receipts. Production is a separate,
|
|
160
|
+
stronger claim that the release is actually live and eligible for release
|
|
161
|
+
notes. In QA1, an independent agent may advance a clean card to QA2; in QA2,
|
|
162
|
+
document the second pass and leave the `🏁 Ready for review` card for the human reviewer.
|
|
159
163
|
|
|
160
164
|
## Writes
|
|
161
165
|
|
package/tool-annotations.js
CHANGED
|
@@ -239,15 +239,22 @@ const IDEMPOTENT_WRITES = new Set([
|
|
|
239
239
|
// Every Starfield board write is governed by a required idempotency key.
|
|
240
240
|
// Retrying the same key replays; it never appends a second transition,
|
|
241
241
|
// comment, criterion, or card.
|
|
242
|
-
'move_work_item', 'set_work_board_column_wip_limit', 'set_work_item_tag', 'set_work_item_kind',
|
|
242
|
+
'move_work_item', 'transfer_work_item_between_boards', 'set_work_board_column_wip_limit', 'set_work_item_tag', 'set_work_item_kind',
|
|
243
243
|
'create_work_item', 'comment_on_work_item', 'flag_work_item',
|
|
244
244
|
'add_work_item_acceptance_criterion',
|
|
245
245
|
// Ticking and un-ticking are both genuinely idempotent, and for different
|
|
246
246
|
// reasons worth keeping straight: a repeat tick is a no-op because the server
|
|
247
247
|
// only writes the ticker, column and note where they were empty, and a repeat
|
|
248
248
|
// un-tick clears columns that are already NULL. Neither can compound.
|
|
249
|
-
'satisfy_work_item_acceptance_criterion',
|
|
249
|
+
'satisfy_work_item_acceptance_criterion', 'record_work_item_criterion_engineering_proof',
|
|
250
250
|
'unsatisfy_work_item_acceptance_criterion',
|
|
251
|
+
'claim_work_item_qa_pass',
|
|
252
|
+
'claim_next_work_item_qa_pass',
|
|
253
|
+
'heartbeat_work_item_qa_pass',
|
|
254
|
+
'attach_work_item_qa_evidence',
|
|
255
|
+
'verify_work_item_qa_evidence_playback',
|
|
256
|
+
'record_work_item_qa_failure_and_release',
|
|
257
|
+
'requeue_work_item_qa_after_fix',
|
|
251
258
|
]);
|
|
252
259
|
|
|
253
260
|
// Non-read tools that create new state / have side effects each call.
|
|
@@ -261,6 +268,7 @@ const NON_IDEMPOTENT_WRITES = new Set([
|
|
|
261
268
|
'warmup_email', 'schedule_demo', 'schedule_meeting', 'connect_workspace',
|
|
262
269
|
'enable_toolset', 'track_conversion', 'log_interaction', 'test_webhook',
|
|
263
270
|
'replay_webhook_delivery', 'upgrade_account',
|
|
271
|
+
'release_work_item_qa_pass',
|
|
264
272
|
]);
|
|
265
273
|
|
|
266
274
|
// Tools that reach beyond the workspace (external enrichment, web, providers).
|