@adrata/adrata-mcp 1.0.2 → 1.0.3
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/access/oauth.js +23 -7
- package/access/resource-metadata.js +9 -1
- package/access/tiers.js +2 -0
- package/api-bridge.js +16 -0
- package/package.json +1 -1
- package/skills/ship-the-card/SKILL.md +22 -0
- package/tool-annotations.js +6 -0
- package/tools/work-board-tools.js +169 -0
package/access/oauth.js
CHANGED
|
@@ -104,6 +104,22 @@ export const OAUTH_WRITE_SCOPE = [
|
|
|
104
104
|
'write:companies', 'write:people', 'write:buyer-groups',
|
|
105
105
|
'write:opportunities', 'write:actions', 'write:tasks',
|
|
106
106
|
'write:partnerships', 'write:sequences', 'write:campaigns', 'write:data',
|
|
107
|
+
// Authorising an external system to write into the workspace — connecting a
|
|
108
|
+
// CRM, or binding a source-control repository to a board. Its absence was not
|
|
109
|
+
// a deliberate least-privilege call, because `read:integrations` was already
|
|
110
|
+
// granted by default: the connection could SEE every integration and connect
|
|
111
|
+
// none of them.
|
|
112
|
+
//
|
|
113
|
+
// Measured 2026-08-29. `POST /api/v1/scm/connections` returned 403
|
|
114
|
+
// insufficient_scope against a session holding ten other write scopes, so the
|
|
115
|
+
// Starfield board could not be wired to GitHub from the only layer that has a
|
|
116
|
+
// connect flow at all — there is no Connections UI for source control. Every
|
|
117
|
+
// card move stayed manual as a result.
|
|
118
|
+
//
|
|
119
|
+
// Still opt-in: this list is only requested by connect_workspace({ writeAccess:
|
|
120
|
+
// true }), so the default grant remains read-only and a leaked token cannot
|
|
121
|
+
// authorise an integration.
|
|
122
|
+
'write:integrations',
|
|
107
123
|
].join(' ');
|
|
108
124
|
|
|
109
125
|
/**
|
|
@@ -576,7 +592,7 @@ async function performTokenRefresh(apiBase, fetchImpl) {
|
|
|
576
592
|
refresh_token: tokens.refreshToken,
|
|
577
593
|
// RFC 8707: keep the same audience binding the initial grant used so the
|
|
578
594
|
// refreshed access token stays bound to the /api/v1/mcp resource.
|
|
579
|
-
resource: tokens.resource || canonicalResource(),
|
|
595
|
+
resource: tokens.resource || canonicalResource(apiBase),
|
|
580
596
|
});
|
|
581
597
|
|
|
582
598
|
let lastError;
|
|
@@ -669,7 +685,7 @@ export async function getValidToken(apiBase, { forceRefresh = false, fetchImpl =
|
|
|
669
685
|
/**
|
|
670
686
|
* Build the OAuth authorization URL.
|
|
671
687
|
*/
|
|
672
|
-
function buildAuthUrl(apiBase, clientId, redirectUri, state, pkce, scope = OAUTH_SCOPE) {
|
|
688
|
+
export function buildAuthUrl(apiBase, clientId, redirectUri, state, pkce, scope = OAUTH_SCOPE) {
|
|
673
689
|
// Use the web app's OAuth authorize page (user-facing login + consent screen)
|
|
674
690
|
const url = new URL(`${OAUTH_BASE_PATH}/authorize`, apiBase);
|
|
675
691
|
url.searchParams.set('client_id', clientId);
|
|
@@ -680,7 +696,7 @@ function buildAuthUrl(apiBase, clientId, redirectUri, state, pkce, scope = OAUTH
|
|
|
680
696
|
url.searchParams.set('source', 'mcp');
|
|
681
697
|
// RFC 8707 Resource Indicator — request a token audience-bound to this MCP
|
|
682
698
|
// resource so it cannot be replayed against a different service.
|
|
683
|
-
url.searchParams.set('resource', canonicalResource());
|
|
699
|
+
url.searchParams.set('resource', canonicalResource(apiBase));
|
|
684
700
|
url.searchParams.set('code_challenge', pkce.challenge);
|
|
685
701
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
686
702
|
return { url: url.toString(), state, codeVerifier: pkce.verifier };
|
|
@@ -853,7 +869,7 @@ export function createOAuthCallbackRequestHandler({
|
|
|
853
869
|
/**
|
|
854
870
|
* Exchange an authorization code for tokens.
|
|
855
871
|
*/
|
|
856
|
-
async function exchangeCode(apiBase, code, clientId, codeVerifier, redirectUri) {
|
|
872
|
+
export async function exchangeCode(apiBase, code, clientId, codeVerifier, redirectUri, fetchImpl = fetch) {
|
|
857
873
|
const url = new URL(`${OAUTH_BASE_PATH}/token`, apiBase);
|
|
858
874
|
const body = new URLSearchParams({
|
|
859
875
|
grant_type: 'authorization_code',
|
|
@@ -861,10 +877,10 @@ async function exchangeCode(apiBase, code, clientId, codeVerifier, redirectUri)
|
|
|
861
877
|
code,
|
|
862
878
|
code_verifier: codeVerifier,
|
|
863
879
|
redirect_uri: redirectUri,
|
|
864
|
-
resource: canonicalResource(),
|
|
880
|
+
resource: canonicalResource(apiBase),
|
|
865
881
|
});
|
|
866
882
|
|
|
867
|
-
const res = await
|
|
883
|
+
const res = await fetchImpl(url.toString(), {
|
|
868
884
|
method: 'POST',
|
|
869
885
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
870
886
|
body,
|
|
@@ -1026,7 +1042,7 @@ export async function connectWorkspace(apiBase, { writeAccess = false } = {}) {
|
|
|
1026
1042
|
clientId,
|
|
1027
1043
|
clientRegistration: registration.clientRegistration,
|
|
1028
1044
|
tokenEndpointAuthMethod: registration.tokenEndpointAuthMethod,
|
|
1029
|
-
resource: canonicalResource(),
|
|
1045
|
+
resource: canonicalResource(apiBase),
|
|
1030
1046
|
apiBase,
|
|
1031
1047
|
connectedAt: new Date().toISOString(),
|
|
1032
1048
|
};
|
|
@@ -35,8 +35,16 @@ export function authorizationServers() {
|
|
|
35
35
|
* The canonical resource identifier for this MCP server. RFC 8707 clients
|
|
36
36
|
* send this as the `resource` parameter so the AS can bind the token's
|
|
37
37
|
* audience to it. Defaults to the REST MCP resource the Rust AS advertises.
|
|
38
|
+
*
|
|
39
|
+
* Client flows pass their explicit API base so a staging authorization request
|
|
40
|
+
* cannot accidentally ask for a production-bound token. Hosted resource-server
|
|
41
|
+
* callers omit it and continue to use the configured authorization server.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} [apiBase] - Explicit authorization-server/API base for a
|
|
44
|
+
* client connection.
|
|
38
45
|
*/
|
|
39
|
-
export function canonicalResource() {
|
|
46
|
+
export function canonicalResource(apiBase) {
|
|
47
|
+
if (apiBase) return `${apiBase.replace(/\/$/, '')}/api/v1/mcp`;
|
|
40
48
|
if (process.env.ADRATA_MCP_RESOURCE) return process.env.ADRATA_MCP_RESOURCE.trim();
|
|
41
49
|
const as = authorizationServers()[0] || 'https://api.adrata.com';
|
|
42
50
|
return `${as.replace(/\/$/, '')}/api/v1/mcp`;
|
package/access/tiers.js
CHANGED
|
@@ -234,6 +234,8 @@ export const TOOL_TIERS = {
|
|
|
234
234
|
set_work_item_kind: TIERS.ENTERPRISE,
|
|
235
235
|
create_work_item: TIERS.ENTERPRISE,
|
|
236
236
|
add_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
237
|
+
satisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
238
|
+
unsatisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
237
239
|
comment_on_work_item: TIERS.ENTERPRISE,
|
|
238
240
|
flag_work_item: TIERS.ENTERPRISE,
|
|
239
241
|
// The containers above the cards, and the "add this to the roadmap" verb.
|
package/api-bridge.js
CHANGED
|
@@ -171,6 +171,14 @@ const ALLOWED_PREFIXES = [
|
|
|
171
171
|
'/api/v1/retention',
|
|
172
172
|
'/api/v1/revenue',
|
|
173
173
|
'/api/v1/revenue-cloud',
|
|
174
|
+
// Source control connections and their repository bindings. The board
|
|
175
|
+
// cannot learn that a pull request merged unless something can create the
|
|
176
|
+
// connection and register the callback, and until this row existed the
|
|
177
|
+
// bridge refused every /scm path — so the receiver at /webhooks/scm sat
|
|
178
|
+
// mounted and reachable with no connection ever pointing at it.
|
|
179
|
+
// The unauthenticated receiver itself is NOT here: it is mounted below the
|
|
180
|
+
// auth layer and is not an agent-callable path.
|
|
181
|
+
'/api/v1/scm',
|
|
174
182
|
'/api/v1/scoring',
|
|
175
183
|
'/api/v1/security',
|
|
176
184
|
'/api/v1/self-service',
|
|
@@ -279,6 +287,14 @@ const PATH_WRITE_SCOPES = [
|
|
|
279
287
|
// All three are listed separately because `matchesPathPrefix` is
|
|
280
288
|
// segment-boundary matched: `/work-boards` does not annex
|
|
281
289
|
// `/work-board-rollups`.
|
|
290
|
+
// Authorising a source-control account is the same act as connecting a CRM —
|
|
291
|
+
// an external system permitted to write into the workspace — so scope_guard
|
|
292
|
+
// maps it onto the integrations family rather than a new one. Deliberately
|
|
293
|
+
// not an `admin:` scope: those refuse first-party bypass and a human session
|
|
294
|
+
// JWT carries an empty permission list, which would 403 every person opening
|
|
295
|
+
// the Connections screen. Mirrors `family_matches(path, "/scm")` in
|
|
296
|
+
// code/api/crates/middleware/src/scope_guard/mod.rs.
|
|
297
|
+
['/api/v1/scm', 'write:integrations'],
|
|
282
298
|
['/api/v1/work-boards', 'write:tasks'],
|
|
283
299
|
['/api/v1/work-items', 'write:tasks'],
|
|
284
300
|
['/api/v1/work-board-rollups', 'write:tasks'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrata/adrata-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. 80+ tools for companies, people, deals, actions, buyer groups, warm intros, webhooks, intelligence, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -110,6 +110,28 @@ Two things to carry back to the card:
|
|
|
110
110
|
session), so the next person at aligning can ask you what you meant; what it
|
|
111
111
|
must not arrive with is a list of outcomes nobody can check.
|
|
112
112
|
|
|
113
|
+
## Tick what you ran
|
|
114
|
+
|
|
115
|
+
Before you move the card on, tick the criteria you actually executed, with
|
|
116
|
+
`satisfy_work_item_acceptance_criterion` and the evidence in `note` — a build
|
|
117
|
+
number, a run link, the caveat that makes it honest. An untouched checkbox says
|
|
118
|
+
nothing about whether the check passed; it says nobody has been near it, which
|
|
119
|
+
is the same thing an abandoned card says.
|
|
120
|
+
|
|
121
|
+
Ticking from a build column records `claimed`, not `verified`, and so does
|
|
122
|
+
ticking your own card at a QA gate. That is the rule, not a refusal: the grade
|
|
123
|
+
is derived from where the card was standing and who you are, and the engineer
|
|
124
|
+
who built the thing cannot verify it. Tick anyway — `claimed` is a real state
|
|
125
|
+
that says the work is believed done, and it is what QA reads before deciding
|
|
126
|
+
what to re-run. What you must not do is tick a box you did not execute to make
|
|
127
|
+
the count look better: the QA gate reads `verified`, so it stops either way, and
|
|
128
|
+
all the false tick achieves is turning an honest "shipped with two open" into a
|
|
129
|
+
claim somebody later believes.
|
|
130
|
+
|
|
131
|
+
If a tick turns out to be wrong, `unsatisfy_work_item_acceptance_criterion`
|
|
132
|
+
takes it off. It clears the note with it, so if the evidence is worth keeping,
|
|
133
|
+
put it on the card with `comment_on_work_item` first.
|
|
134
|
+
|
|
113
135
|
## Submit it
|
|
114
136
|
|
|
115
137
|
When the change is ready for review, `move_work_item` it onward — no `claim`
|
package/tool-annotations.js
CHANGED
|
@@ -242,6 +242,12 @@ const IDEMPOTENT_WRITES = new Set([
|
|
|
242
242
|
'move_work_item', '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
|
+
// Ticking and un-ticking are both genuinely idempotent, and for different
|
|
246
|
+
// reasons worth keeping straight: a repeat tick is a no-op because the server
|
|
247
|
+
// only writes the ticker, column and note where they were empty, and a repeat
|
|
248
|
+
// un-tick clears columns that are already NULL. Neither can compound.
|
|
249
|
+
'satisfy_work_item_acceptance_criterion',
|
|
250
|
+
'unsatisfy_work_item_acceptance_criterion',
|
|
245
251
|
]);
|
|
246
252
|
|
|
247
253
|
// Non-read tools that create new state / have side effects each call.
|
|
@@ -131,6 +131,19 @@ export function describeMissingAcceptanceCriteria(criteria) {
|
|
|
131
131
|
return 'This card would have no executable acceptance criteria. It can be captured, but it is not ready for build or QA until at least one where/when/then criterion is added.';
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The satisfaction sub-resource of one criterion, POSTed to tick and DELETEd to
|
|
136
|
+
* un-tick.
|
|
137
|
+
*
|
|
138
|
+
* One function for both verbs because they address the SAME resource: a tick
|
|
139
|
+
* and its removal that disagreed about the path would fail asymmetrically —
|
|
140
|
+
* ticks landing and un-ticks 404ing — which is the failure that leaves a board
|
|
141
|
+
* with checkboxes nobody can clear.
|
|
142
|
+
*/
|
|
143
|
+
export function criterionSatisfactionPath(itemId, criterionId) {
|
|
144
|
+
return `/api/v1/work-items/${encodeURIComponent(itemId)}/acceptance-criteria/${encodeURIComponent(criterionId)}/satisfaction`;
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
/** One target stage's WIP state before and after a proposed move. */
|
|
135
148
|
export function projectColumnWip({ limit, count, cardAlreadyThere, truncated = false }) {
|
|
136
149
|
const projected = count + (cardAlreadyThere ? 0 : 1);
|
|
@@ -885,6 +898,160 @@ YOU DO NOT NEED TO SAY WHO IS CREATING IT. The card records its creator from you
|
|
|
885
898
|
return ok({ added: true, criterion: data?.data });
|
|
886
899
|
}
|
|
887
900
|
);
|
|
901
|
+
|
|
902
|
+
server.tool(
|
|
903
|
+
'satisfy_work_item_acceptance_criterion',
|
|
904
|
+
`Tick one acceptance criterion, with the evidence that made you believe it. This is the act that turns a checklist into proof: a criterion nobody ticks is indistinguishable from one nobody ran, and a board full of untouched checkboxes reports the same thing whether the work was verified or abandoned.
|
|
905
|
+
|
|
906
|
+
WHERE YOU TICK FROM DECIDES WHAT THE TICK IS WORTH, and it is not a permission you can ask for. The grade is derived on every read from the card's CURRENT column and from who you are:
|
|
907
|
+
|
|
908
|
+
• \`verified\` — ticked while the card stands in a QA stage ("Staging QA1" or "Staging QA2"), by somebody who neither wrote the criterion nor owns the card.
|
|
909
|
+
• \`claimed\` — everything else. Ticked in a build column, or ticked at a QA gate by the card's owner or by the person who authored the check. A real and useful state: it says the work is believed done. It is self-assessment, and the board never counts it as verification.
|
|
910
|
+
|
|
911
|
+
So the engineer who built the card CANNOT verify their own work, and neither can whoever wrote the criterion — not because the tick is refused, but because it grades as \`claimed\` however it is worded. Nothing here fails on that account; you get the criterion back carrying a grade, and the grade may not be the one you expected. If you want \`verified\`, the card has to be standing at a QA gate and somebody else has to be doing the ticking.
|
|
912
|
+
|
|
913
|
+
TICK WHAT YOU RAN, not what you believe. The card face prints met over total; the QA exit gate reads the VERIFIED count. So a card whose boxes were all ticked by its own author reads 5/5 on the face and still stops the gate — which is the design working. Ticking without running the check converts an honest "we shipped with two open" into a false "all met", and the false version is the one that gets believed later.
|
|
914
|
+
|
|
915
|
+
FIRST TICK WINS. The server records the ticker, the column and the note only where they were empty, so a second person ticking an already-ticked box cannot overwrite the first person's evidence, and re-ticking is a no-op rather than an upgrade. To re-tick under different circumstances, un-tick it first with unsatisfy_work_item_acceptance_criterion — that clears the ticker, the column and the note together, so the next tick cannot inherit somebody else's evidence.${GOVERNED_NOTE}`,
|
|
916
|
+
{
|
|
917
|
+
itemId: z.string().describe('Card id.'),
|
|
918
|
+
criterionId: z
|
|
919
|
+
.string()
|
|
920
|
+
.describe('Criterion id from list_work_item_acceptance_criteria — the `id`, not the `ordinal`.'),
|
|
921
|
+
note: z
|
|
922
|
+
.string()
|
|
923
|
+
.optional()
|
|
924
|
+
.describe(
|
|
925
|
+
'OPTIONAL, and the most useful field here: the evidence. A build number, a PR or run link, or the caveat that makes the tick honest ("only checked on Safari"). Stored on the criterion and shown beside it, so it is what a release review reads instead of taking the tick on trust. Omit it when there is genuinely nothing to add — an invented note is worse than none.'
|
|
926
|
+
),
|
|
927
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to record the tick.'),
|
|
928
|
+
approved: z.boolean().optional().describe('Required true for a live tick.'),
|
|
929
|
+
reason: z
|
|
930
|
+
.string()
|
|
931
|
+
.optional()
|
|
932
|
+
.describe(
|
|
933
|
+
'Required for a live tick: the audit reason — how you checked this, not what the criterion already says. The evidence a human reads on the card is `note`.'
|
|
934
|
+
),
|
|
935
|
+
idempotencyKey: z
|
|
936
|
+
.string()
|
|
937
|
+
.optional()
|
|
938
|
+
.describe('Required for a live tick. Reuse the SAME key on retry.'),
|
|
939
|
+
},
|
|
940
|
+
async (args) => {
|
|
941
|
+
const unauditable = args.dryRun === false ? describeUnauditableReason(args.reason) : null;
|
|
942
|
+
if (unauditable) return ok({ error: true, message: unauditable });
|
|
943
|
+
|
|
944
|
+
const path = criterionSatisfactionPath(args.itemId, args.criterionId);
|
|
945
|
+
const preview = validateApiBridgeRequest({
|
|
946
|
+
method: 'POST',
|
|
947
|
+
path,
|
|
948
|
+
dryRun: args.dryRun,
|
|
949
|
+
approved: args.approved,
|
|
950
|
+
reason: args.reason,
|
|
951
|
+
idempotencyKey: args.idempotencyKey,
|
|
952
|
+
grantedScope: getGrantedScope(),
|
|
953
|
+
});
|
|
954
|
+
if (preview?.dryRun) {
|
|
955
|
+
return ok({
|
|
956
|
+
...preview,
|
|
957
|
+
wouldSatisfy: {
|
|
958
|
+
itemId: args.itemId,
|
|
959
|
+
criterionId: args.criterionId,
|
|
960
|
+
note: args.note ?? null,
|
|
961
|
+
},
|
|
962
|
+
// Said in the preview because the preview is where a caller decides
|
|
963
|
+
// whether to go live, and the one thing it CANNOT tell them is the
|
|
964
|
+
// answer they want. Predicting the grade here would need the card's
|
|
965
|
+
// current column and the criterion's author, and a prediction made
|
|
966
|
+
// from a stale read is worse than an honest refusal to predict.
|
|
967
|
+
gradeIsDerived:
|
|
968
|
+
'The grade is computed at read time from the column this card is standing in and from who you are, so this preview cannot tell you which you will get. Ticking from a QA stage, as somebody who is neither the criterion\'s author nor the card\'s owner, grades `verified`; anything else grades `claimed`.',
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const data = await api('POST', path, {
|
|
973
|
+
// The note is the only field this endpoint takes, and it is optional:
|
|
974
|
+
// `SatisfyCriterionRequest` defaults it, so an omitted note is a valid
|
|
975
|
+
// body rather than a missing one. Sending `{}` is deliberate — a tick
|
|
976
|
+
// with nothing to add must stay recordable, because the alternative is
|
|
977
|
+
// an agent inventing evidence to satisfy a required field.
|
|
978
|
+
body: args.note === undefined ? {} : { note: args.note },
|
|
979
|
+
headers: buildMutationHeaders(args),
|
|
980
|
+
});
|
|
981
|
+
const criterion = data?.data;
|
|
982
|
+
return ok({
|
|
983
|
+
satisfied: true,
|
|
984
|
+
criterion,
|
|
985
|
+
// The grade is the answer, and it is the server's to give. Echoing an
|
|
986
|
+
// optimistic "verified" from the request would be this tool asserting
|
|
987
|
+
// the exact fact the grading rule exists to withhold.
|
|
988
|
+
grade: criterion?.grade,
|
|
989
|
+
...(criterion?.grade === 'claimed'
|
|
990
|
+
? {
|
|
991
|
+
gradeNote:
|
|
992
|
+
'Recorded as `claimed`, not `verified`: either the card was not standing in a QA stage, or you wrote this criterion or own the card. That is the rule working rather than a failure — the tick stands and the card face counts it. Verification needs a QA stage and a different pair of hands.',
|
|
993
|
+
}
|
|
994
|
+
: {}),
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
server.tool(
|
|
1000
|
+
'unsatisfy_work_item_acceptance_criterion',
|
|
1001
|
+
`Un-tick one acceptance criterion, back to \`unmet\`. Use it when the check turns out not to hold, when it was ticked against the wrong build, or when it was ticked from a build column and has to be re-run at the gate to count as verified.
|
|
1002
|
+
|
|
1003
|
+
This exists so that ticking is safe to do. A box nobody can clear is a box people hesitate to tick, and that hesitation is exactly how a board ends up with a full checklist that nobody has ever touched — so shipping the tick without its counterpart would have left the same problem in a new shape.
|
|
1004
|
+
|
|
1005
|
+
It clears the WHOLE circumstance together — the ticker, the column, the note and the timestamp — never just the timestamp. Otherwise the next tick would inherit somebody else's evidence: a criterion re-ticked in Aligning would keep reading as verified in QA1, attributed to a person who was not there. So the cost is real, and it is the note: the first ticker's evidence is gone. If it is worth keeping, put it on the card with comment_on_work_item BEFORE you clear it.
|
|
1006
|
+
|
|
1007
|
+
Anyone who can see the card may un-tick, including somebody undoing another person's tick. The audit reason is the only record of why, so write it for them.${GOVERNED_NOTE}`,
|
|
1008
|
+
{
|
|
1009
|
+
itemId: z.string().describe('Card id.'),
|
|
1010
|
+
criterionId: z
|
|
1011
|
+
.string()
|
|
1012
|
+
.describe('Criterion id from list_work_item_acceptance_criteria — the `id`, not the `ordinal`.'),
|
|
1013
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to clear the tick.'),
|
|
1014
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
1015
|
+
reason: z
|
|
1016
|
+
.string()
|
|
1017
|
+
.optional()
|
|
1018
|
+
.describe(
|
|
1019
|
+
'Required for a live change: why this tick is coming off — the check failed on a later build, it was run against the wrong environment, it needs re-running at the gate. This is the only record the original ticker will have of losing their evidence.'
|
|
1020
|
+
),
|
|
1021
|
+
idempotencyKey: z
|
|
1022
|
+
.string()
|
|
1023
|
+
.optional()
|
|
1024
|
+
.describe('Required for a live change. Reuse the SAME key on retry.'),
|
|
1025
|
+
},
|
|
1026
|
+
async (args) => {
|
|
1027
|
+
const unauditable = args.dryRun === false ? describeUnauditableReason(args.reason) : null;
|
|
1028
|
+
if (unauditable) return ok({ error: true, message: unauditable });
|
|
1029
|
+
|
|
1030
|
+
const path = criterionSatisfactionPath(args.itemId, args.criterionId);
|
|
1031
|
+
const preview = validateApiBridgeRequest({
|
|
1032
|
+
method: 'DELETE',
|
|
1033
|
+
path,
|
|
1034
|
+
dryRun: args.dryRun,
|
|
1035
|
+
approved: args.approved,
|
|
1036
|
+
reason: args.reason,
|
|
1037
|
+
idempotencyKey: args.idempotencyKey,
|
|
1038
|
+
grantedScope: getGrantedScope(),
|
|
1039
|
+
});
|
|
1040
|
+
if (preview?.dryRun) {
|
|
1041
|
+
return ok({
|
|
1042
|
+
...preview,
|
|
1043
|
+
wouldUnsatisfy: { itemId: args.itemId, criterionId: args.criterionId },
|
|
1044
|
+
clears:
|
|
1045
|
+
'The ticker, the column the tick happened in, the evidence note, and the timestamp — all four together, so a later tick cannot inherit this one. Copy the note onto the card first if it is worth keeping.',
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const data = await api('DELETE', path, { headers: buildMutationHeaders(args) });
|
|
1050
|
+
const criterion = data?.data;
|
|
1051
|
+
return ok({ unsatisfied: true, criterion, grade: criterion?.grade });
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
|
|
888
1055
|
server.tool(
|
|
889
1056
|
'comment_on_work_item',
|
|
890
1057
|
`Say something on a card: a question, a finding, or the reason a QA pass sent it back. This is the ONLY place to put a fact that contradicts the card — the ship-the-card skill tells you to say so on the card rather than silently fixing something else, and this is where that goes. Do NOT overwrite the card's body to make the point: the body is the original request, and rewriting it destroys the evidence of what was actually asked for.
|
|
@@ -1022,6 +1189,8 @@ export const WORK_BOARD_TOOL_NAMES = [
|
|
|
1022
1189
|
'set_work_item_kind',
|
|
1023
1190
|
'create_work_item',
|
|
1024
1191
|
'add_work_item_acceptance_criterion',
|
|
1192
|
+
'satisfy_work_item_acceptance_criterion',
|
|
1193
|
+
'unsatisfy_work_item_acceptance_criterion',
|
|
1025
1194
|
'get_work_item_comments',
|
|
1026
1195
|
'comment_on_work_item',
|
|
1027
1196
|
'flag_work_item',
|