@adrata/adrata-mcp 1.0.8 → 1.0.30
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 +27 -0
- package/access/tiers.js +14 -0
- package/api-bridge.js +26 -0
- package/governance/governed-args.js +7 -2
- package/http/edge-block.js +16 -7
- package/http/rate-limit.js +69 -6
- package/package.json +3 -3
- package/security.js +167 -14
- package/server.js +35 -23
- package/server.json +2 -2
- package/skills/qa-the-card/SKILL.md +40 -4
- package/tool-annotations.js +23 -0
- package/tools/provisioning/onboarding-tools.js +730 -0
- package/tools/source-control/connection-tools.js +117 -1
- package/tools/work-board-tools.js +441 -25
- package/tools/work-hub/audit.js +74 -8
- package/tools/work-hub/criteria-quality.js +212 -0
- package/toolsets/communications.js +42 -64
- package/toolsets/spaces.js +10 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { auditWorkHubBoards, deliveryContradictions } from './work-hub/audit.js';
|
|
2
|
+
import { describeCriteriaQuality, inspectCriteria } from './work-hub/criteria-quality.js';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { readFile, stat } from 'node:fs/promises';
|
|
4
5
|
import { basename, resolve } from 'node:path';
|
|
@@ -273,7 +274,7 @@ export function registerWorkBoardTools(
|
|
|
273
274
|
// public id and the handler adds the three private headers at the last hop.
|
|
274
275
|
const workerLeaseCapabilities = new Map();
|
|
275
276
|
|
|
276
|
-
function rememberWorkerLease(itemId, grant, claimIdempotencyKey) {
|
|
277
|
+
function rememberWorkerLease(itemId, grant, claimIdempotencyKey, workerLabel) {
|
|
277
278
|
if (!grant?.leaseId || !grant?.leaseToken || !Number.isInteger(grant?.fencingToken)) {
|
|
278
279
|
throw new Error('The API returned an incomplete worker-lease grant; no capability was stored.');
|
|
279
280
|
}
|
|
@@ -282,18 +283,32 @@ export function registerWorkBoardTools(
|
|
|
282
283
|
leaseToken: grant.leaseToken,
|
|
283
284
|
fencingToken: grant.fencingToken,
|
|
284
285
|
claimIdempotencyKey,
|
|
286
|
+
workerLabel,
|
|
285
287
|
});
|
|
286
288
|
const { leaseToken: _secret, fencingToken: _fence, qaRequirements, currentCriteria, ...lease } =
|
|
287
289
|
grant;
|
|
288
290
|
return { lease, qaRequirements, currentCriteria };
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
|
|
293
|
+
// ONE PASS PER WORKER, NOT PER PROCESS. A fleet runs many QA lanes through a
|
|
294
|
+
// single connector, so scanning every capability in the map made lane A's
|
|
295
|
+
// card refuse lane B — naming a card lane B had never seen and telling it to
|
|
296
|
+
// "transition or release" a peer's pass. That serialises the parallel QA
|
|
297
|
+
// drain the atomic claim exists to make safe, and it does so silently: the
|
|
298
|
+
// refusal reads exactly like ordinary contention for one card.
|
|
299
|
+
//
|
|
300
|
+
// The worker is what the guard was always about, and `workerLabel` is how a
|
|
301
|
+
// worker names itself. A capability stored without one cannot be attributed,
|
|
302
|
+
// so it still conflicts — an unattributable hold fails toward refusing, which
|
|
303
|
+
// costs a retry, rather than toward a double hold, which costs a lost lease.
|
|
304
|
+
function conflictingHeldCard(itemId, claimIdempotencyKey, workerLabel) {
|
|
292
305
|
for (const [heldItemId, capability] of workerLeaseCapabilities) {
|
|
293
306
|
const sameCard = itemId != null && heldItemId === itemId;
|
|
294
307
|
const sameClaim =
|
|
295
308
|
claimIdempotencyKey != null && capability.claimIdempotencyKey === claimIdempotencyKey;
|
|
296
|
-
|
|
309
|
+
const sameWorker =
|
|
310
|
+
capability.workerLabel == null || capability.workerLabel === workerLabel;
|
|
311
|
+
if (!sameCard && !sameClaim && sameWorker) return heldItemId;
|
|
297
312
|
}
|
|
298
313
|
return null;
|
|
299
314
|
}
|
|
@@ -510,7 +525,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
510
525
|
idempotencyKey: z.string().optional(),
|
|
511
526
|
},
|
|
512
527
|
async (args) => {
|
|
513
|
-
const heldItemId = conflictingHeldCard(args.itemId, args.idempotencyKey);
|
|
528
|
+
const heldItemId = conflictingHeldCard(args.itemId, args.idempotencyKey, args.workerLabel);
|
|
514
529
|
if (heldItemId) {
|
|
515
530
|
return ok({
|
|
516
531
|
error: true,
|
|
@@ -546,7 +561,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
546
561
|
body,
|
|
547
562
|
headers: buildMutationHeaders(args),
|
|
548
563
|
});
|
|
549
|
-
const visible = rememberWorkerLease(args.itemId, data?.data, args.idempotencyKey);
|
|
564
|
+
const visible = rememberWorkerLease(args.itemId, data?.data, args.idempotencyKey, args.workerLabel);
|
|
550
565
|
return ok({ claimed: true, ...visible });
|
|
551
566
|
}
|
|
552
567
|
);
|
|
@@ -566,7 +581,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
566
581
|
idempotencyKey: z.string().optional(),
|
|
567
582
|
},
|
|
568
583
|
async (args) => {
|
|
569
|
-
const heldItemId = conflictingHeldCard(null, args.idempotencyKey);
|
|
584
|
+
const heldItemId = conflictingHeldCard(null, args.idempotencyKey, args.workerLabel);
|
|
570
585
|
if (heldItemId) {
|
|
571
586
|
return ok({
|
|
572
587
|
error: true,
|
|
@@ -624,7 +639,12 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
624
639
|
: `No eligible unclaimed ${args.product} ${args.qaGate} pass remains on any visible board.`,
|
|
625
640
|
});
|
|
626
641
|
}
|
|
627
|
-
const visible = rememberWorkerLease(
|
|
642
|
+
const visible = rememberWorkerLease(
|
|
643
|
+
selected.workItemId,
|
|
644
|
+
selected.grant,
|
|
645
|
+
args.idempotencyKey,
|
|
646
|
+
args.workerLabel
|
|
647
|
+
);
|
|
628
648
|
return ok({
|
|
629
649
|
claimed: true,
|
|
630
650
|
skippedBlocked,
|
|
@@ -881,7 +901,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
881
901
|
|
|
882
902
|
server.tool(
|
|
883
903
|
'list_work_item_acceptance_criteria',
|
|
884
|
-
'Read the ordered, executable definition of done for one card. Each criterion says where to check, any starting state, what action to perform, and the observable result. A zero-length list is a finding: nobody has stated how QA can decide the card is done.',
|
|
904
|
+
'Read the ordered, executable definition of done for one card. Each criterion says where to check, any starting state, what action to perform, and the observable result. A zero-length list is a finding: nobody has stated how QA can decide the card is done. `criteriaQuality` names criteria that cannot fail as written — an unfalsifiable result, a location that is not a place to stand, an unfinished clause. Correct those before executing them; a criterion turned green by reading it generously is worth less than an open one.',
|
|
885
905
|
{ itemId: z.string().describe('Card id.') },
|
|
886
906
|
async (args) => {
|
|
887
907
|
const data = await api(
|
|
@@ -889,7 +909,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
889
909
|
`/api/v1/work-items/${encodeURIComponent(args.itemId)}/acceptance-criteria`
|
|
890
910
|
);
|
|
891
911
|
const criteria = data?.data ?? [];
|
|
892
|
-
return ok({ count: criteria.length, criteria });
|
|
912
|
+
return ok({ count: criteria.length, criteria, criteriaQuality: inspectCriteria(criteria) });
|
|
893
913
|
}
|
|
894
914
|
);
|
|
895
915
|
|
|
@@ -909,7 +929,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
909
929
|
|
|
910
930
|
server.tool(
|
|
911
931
|
'audit_work_hub',
|
|
912
|
-
`Audit every visible Starfield board as an operating ledger. Returns named findings for active cards without owners, active passes without handlers, missing definitions of done, missing work types or context, stale stages, and
|
|
932
|
+
`Audit every visible Starfield board as an operating ledger. Returns named findings for active cards without owners, active passes without handlers, missing definitions of done, missing work types or context, stale stages, truncated reads, and boards that could not be read at all. It does not award a vanity score and it does not mutate anything. A clean result means every board it COULD read has the minimum workflow facts required to operate from it — read \`boards\` against \`boardsExpected\` before treating it as coverage. Deployment truth remains a separate exact-SHA fact available through get_work_item_delivery_evidence.`,
|
|
913
933
|
{},
|
|
914
934
|
async () => {
|
|
915
935
|
const listed = await api('GET', '/api/v1/work-boards');
|
|
@@ -920,7 +940,11 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
920
940
|
return detail?.data;
|
|
921
941
|
})
|
|
922
942
|
);
|
|
923
|
-
|
|
943
|
+
// `refs.length` travels with the payload so a board whose detail read came
|
|
944
|
+
// back empty is REPORTED as unread rather than silently dropped by the
|
|
945
|
+
// filter. Auditing one of four boards and saying "trustworthy" is the
|
|
946
|
+
// single-board coverage claim this repo already warns about.
|
|
947
|
+
return ok(auditWorkHubBoards(boards.filter(Boolean), { expectedBoards: refs.length }));
|
|
924
948
|
}
|
|
925
949
|
);
|
|
926
950
|
|
|
@@ -1479,11 +1503,25 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1479
1503
|
return ok({
|
|
1480
1504
|
moved: true,
|
|
1481
1505
|
claimed: args.claim === true,
|
|
1482
|
-
//
|
|
1483
|
-
//
|
|
1506
|
+
// Read back from the SERVER, never `args.receipt !== undefined`. That
|
|
1507
|
+
// echo reported the request rather than the answer, and the two
|
|
1508
|
+
// disagree in all three directions: `receipt: {}` points at nothing and
|
|
1509
|
+
// the server grades it `server_recorded`; offering no receipt at all
|
|
1510
|
+
// still writes a `server_recorded` row, so `false` was wrong; and a
|
|
1511
|
+
// REORDER writes no receipt whatsoever, because the API guards the
|
|
1512
|
+
// write with `if changed_column` — yet a receipt passed alongside one
|
|
1513
|
+
// still reported `true`.
|
|
1514
|
+
//
|
|
1515
|
+
// Still a BOOLEAN and still never the receipt's contents: repeating a
|
|
1484
1516
|
// session pointer back into a conversation transcript is the leak this
|
|
1485
1517
|
// whole field is shaped to avoid.
|
|
1486
|
-
receiptRecorded:
|
|
1518
|
+
receiptRecorded: data?.receiptEvidenceSource != null,
|
|
1519
|
+
// Whether that receipt points at anything, which is a different
|
|
1520
|
+
// question from whether one exists — and it is the one the gaps query
|
|
1521
|
+
// asks. `attested` only when the caller's references actually carried a
|
|
1522
|
+
// value; `server_recorded` when the server wrote the row on its own
|
|
1523
|
+
// observation; null when no receipt was written for this call.
|
|
1524
|
+
receiptEvidenceSource: data?.receiptEvidenceSource ?? null,
|
|
1487
1525
|
// Read back from the server rather than echoed from the request, because
|
|
1488
1526
|
// the two halves of a claim are decided server-side and an agent that
|
|
1489
1527
|
// assumed "claimed" meant "mine now" would report a QA pick-up as having
|
|
@@ -1600,7 +1638,12 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1600
1638
|
return ok({
|
|
1601
1639
|
transferred: true,
|
|
1602
1640
|
identityPreserved: true,
|
|
1603
|
-
|
|
1641
|
+
// The server's verdict, not an echo of the request. See the move tool
|
|
1642
|
+
// above for why those differ. A transfer always changes board AND
|
|
1643
|
+
// column, so unlike a move it always writes a receipt — but whether
|
|
1644
|
+
// that receipt points at anything is still the server's call.
|
|
1645
|
+
receiptRecorded: data?.receiptEvidenceSource != null,
|
|
1646
|
+
receiptEvidenceSource: data?.receiptEvidenceSource ?? null,
|
|
1604
1647
|
item: data?.data,
|
|
1605
1648
|
});
|
|
1606
1649
|
}
|
|
@@ -1853,10 +1896,12 @@ YOU DO NOT NEED TO SAY WHO IS CREATING IT. The card records its creator from you
|
|
|
1853
1896
|
acceptanceCriteria: z
|
|
1854
1897
|
.array(
|
|
1855
1898
|
z.object({
|
|
1856
|
-
whereText: z.string().describe(
|
|
1899
|
+
whereText: z.string().describe(
|
|
1900
|
+
'Surface, environment, account, or role to check. A place the next reviewer can stand, not the whole product — "the app" tells them nothing.'
|
|
1901
|
+
),
|
|
1857
1902
|
givenText: z.string().optional().describe('Starting state, when one is required.'),
|
|
1858
1903
|
whenText: z.string().describe('Action the verifier performs.'),
|
|
1859
|
-
thenText: z.string().describe('Observable result that must follow.'),
|
|
1904
|
+
thenText: z.string().describe('Observable result that must follow. Name a checkable value, count, state, or exact message. A criterion that cannot fail cannot establish completion.'),
|
|
1860
1905
|
verificationRoute: z
|
|
1861
1906
|
.enum(['product', 'engineering', 'both'])
|
|
1862
1907
|
.describe(
|
|
@@ -1934,6 +1979,11 @@ YOU DO NOT NEED TO SAY WHO IS CREATING IT. The card records its creator from you
|
|
|
1934
1979
|
// capture is how work stops reaching the board at all.
|
|
1935
1980
|
note: describeMissingBody(args.body),
|
|
1936
1981
|
readinessNote: describeMissingAcceptanceCriteria(args.acceptanceCriteria),
|
|
1982
|
+
// Advisory, and only ever present when it has something to say. The
|
|
1983
|
+
// preview is where an author is already deciding whether to approve,
|
|
1984
|
+
// and a criterion that cannot fail is cheapest to fix before the card
|
|
1985
|
+
// exists rather than after QA has tried to execute it.
|
|
1986
|
+
criteriaQualityNote: describeCriteriaQuality(args.acceptanceCriteria),
|
|
1937
1987
|
});
|
|
1938
1988
|
}
|
|
1939
1989
|
|
|
@@ -1980,10 +2030,12 @@ YOU DO NOT NEED TO SAY WHO IS CREATING IT. The card records its creator from you
|
|
|
1980
2030
|
`Add one executable acceptance criterion to a card. This is grooming, not a comment: write where the check runs, the action, the observable result, and its proof route. verificationRoute is REQUIRED -- an omitted route silently became product, which then demands a browser recording for a criterion about a CI job. engineering and both require a plain-language engineeringReason. The server stores the criterion and its initial route audit receipt atomically.${GOVERNED_NOTE}`,
|
|
1981
2031
|
{
|
|
1982
2032
|
itemId: z.string().describe('Card id.'),
|
|
1983
|
-
whereText: z.string().describe(
|
|
2033
|
+
whereText: z.string().describe(
|
|
2034
|
+
'Surface, environment, account, or role to check. A place the next reviewer can stand, not the whole product — "the app" tells them nothing.'
|
|
2035
|
+
),
|
|
1984
2036
|
givenText: z.string().optional().describe('Starting state, when one is required.'),
|
|
1985
2037
|
whenText: z.string().describe('Action the verifier performs.'),
|
|
1986
|
-
thenText: z.string().describe('Observable result that must follow.'),
|
|
2038
|
+
thenText: z.string().describe('Observable result that must follow. Name a checkable value, count, state, or exact message. A criterion that cannot fail cannot establish completion.'),
|
|
1987
2039
|
verificationRoute: z
|
|
1988
2040
|
.enum(['product', 'engineering', 'both'])
|
|
1989
2041
|
.describe(
|
|
@@ -2025,7 +2077,12 @@ YOU DO NOT NEED TO SAY WHO IS CREATING IT. The card records its creator from you
|
|
|
2025
2077
|
verificationRoute: args.verificationRoute,
|
|
2026
2078
|
engineeringReason: args.engineeringReason,
|
|
2027
2079
|
};
|
|
2028
|
-
if (preview?.dryRun)
|
|
2080
|
+
if (preview?.dryRun)
|
|
2081
|
+
return ok({
|
|
2082
|
+
...preview,
|
|
2083
|
+
wouldAdd: { itemId: args.itemId, criterion },
|
|
2084
|
+
criteriaQualityNote: describeCriteriaQuality([criterion]),
|
|
2085
|
+
});
|
|
2029
2086
|
const data = await api('POST', path, {
|
|
2030
2087
|
body: criterion,
|
|
2031
2088
|
headers: buildMutationHeaders(args),
|
|
@@ -2112,6 +2169,158 @@ The server refuses the change when the card is standing at a QA gate AND you are
|
|
|
2112
2169
|
}
|
|
2113
2170
|
);
|
|
2114
2171
|
|
|
2172
|
+
server.tool(
|
|
2173
|
+
'edit_work_item_acceptance_criterion',
|
|
2174
|
+
`Rewrite the WORDS of one existing acceptance criterion — its \`whereText\`, \`whenText\`, \`thenText\` or \`givenText\`. Use it when a criterion says the wrong thing: its \`whereText\` names a surface this card does not own, its \`thenText\` asks for something no observation could measure, or it cites a count, fixture or file name that has since changed and would now fail a reviewer who is looking at the right thing.
|
|
2175
|
+
|
|
2176
|
+
REWRITING ANY OF THE FOUR TEXTS CLEARS THE CRITERION'S SATISFACTION. The ticker, the column, the note, the dwell and the evidence link are all set back to NULL, and the card drops below its gate. That is deliberate and it is not a bug to work around: a tick is evidence about a SENTENCE, and the moment the sentence changes the evidence no longer refers to anything. Reordering alone does not clear it — only a change to the words does. So edit at grooming, before a pass has spent anything; editing a verified criterion costs a real recorded journey.
|
|
2177
|
+
|
|
2178
|
+
WHAT THIS TOOL MUST NOT BE USED FOR, and it is the only way it can do damage: narrowing a criterion because the product is failing it. If the product does not do what the criterion says, the fix is the product, or a bounce naming the criterion — never a sentence rewritten until today's behaviour passes. Afterwards a laundered criterion is indistinguishable from one that always said that, except in the receipt, so the receipt is the only thing standing between this tool and a definition of done that quietly tracks whatever was built.
|
|
2179
|
+
|
|
2180
|
+
PREFER THIS TO DELETION WHENEVER THE INTENT IS REAL. Most criteria that look impossible are merely mis-scoped — they ask this card to establish something the whole board owns. Rewriting keeps the intent on the record where a reader can still see what was wanted; deleting throws it away.${GOVERNED_NOTE}`,
|
|
2181
|
+
{
|
|
2182
|
+
itemId: z.string().describe('Card id.'),
|
|
2183
|
+
criterionId: z
|
|
2184
|
+
.string()
|
|
2185
|
+
.describe(
|
|
2186
|
+
'Criterion id from list_work_item_acceptance_criteria — the `id`, not the `ordinal`.'
|
|
2187
|
+
),
|
|
2188
|
+
whereText: z
|
|
2189
|
+
.string()
|
|
2190
|
+
.optional()
|
|
2191
|
+
.describe('New WHERE: the surface or system a reviewer looks at. Omit to leave unchanged.'),
|
|
2192
|
+
whenText: z
|
|
2193
|
+
.string()
|
|
2194
|
+
.optional()
|
|
2195
|
+
.describe('New WHEN: the action the reviewer takes. Omit to leave unchanged.'),
|
|
2196
|
+
thenText: z
|
|
2197
|
+
.string()
|
|
2198
|
+
.optional()
|
|
2199
|
+
.describe(
|
|
2200
|
+
'New THEN: the observable result. Must be something a reviewer could see and ' +
|
|
2201
|
+
'disagree about. Omit to leave unchanged.'
|
|
2202
|
+
),
|
|
2203
|
+
givenText: z
|
|
2204
|
+
.string()
|
|
2205
|
+
.optional()
|
|
2206
|
+
.describe('New GIVEN: the precondition. Omit to leave unchanged.'),
|
|
2207
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to apply it.'),
|
|
2208
|
+
approved: z.boolean().optional().describe('Required true for a live write.'),
|
|
2209
|
+
reason: z
|
|
2210
|
+
.string()
|
|
2211
|
+
.optional()
|
|
2212
|
+
.describe(
|
|
2213
|
+
'Required for a live write: what the criterion said, what it will say, and why ' +
|
|
2214
|
+
'the old wording could not be reviewed as written.'
|
|
2215
|
+
),
|
|
2216
|
+
idempotencyKey: z.string().optional().describe('Required for a live write. Reuse on retry.'),
|
|
2217
|
+
},
|
|
2218
|
+
async (args) => {
|
|
2219
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/acceptance-criteria/${encodeURIComponent(args.criterionId)}`;
|
|
2220
|
+
const preview = validateApiBridgeRequest({
|
|
2221
|
+
method: 'PATCH',
|
|
2222
|
+
path,
|
|
2223
|
+
dryRun: args.dryRun,
|
|
2224
|
+
approved: args.approved,
|
|
2225
|
+
reason: args.reason,
|
|
2226
|
+
idempotencyKey: args.idempotencyKey,
|
|
2227
|
+
grantedScope: getGrantedScope(),
|
|
2228
|
+
});
|
|
2229
|
+
// Only the parts the caller actually named. An absent key means "leave this
|
|
2230
|
+
// column exactly as it is" on the server; sending all four would clear the
|
|
2231
|
+
// criterion's satisfaction even for a caller who changed one word.
|
|
2232
|
+
const edit = {};
|
|
2233
|
+
if (args.whereText !== undefined) edit.whereText = args.whereText;
|
|
2234
|
+
if (args.whenText !== undefined) edit.whenText = args.whenText;
|
|
2235
|
+
if (args.thenText !== undefined) edit.thenText = args.thenText;
|
|
2236
|
+
if (args.givenText !== undefined) edit.givenText = args.givenText;
|
|
2237
|
+
if (Object.keys(edit).length === 0) {
|
|
2238
|
+
return ok({
|
|
2239
|
+
edited: false,
|
|
2240
|
+
refused:
|
|
2241
|
+
'No new text was given, so there is nothing to change. Name at least one of ' +
|
|
2242
|
+
'whereText, whenText, thenText or givenText.',
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
if (preview?.dryRun) {
|
|
2246
|
+
return ok({
|
|
2247
|
+
...preview,
|
|
2248
|
+
wouldEdit: { itemId: args.itemId, criterionId: args.criterionId, edit },
|
|
2249
|
+
warning:
|
|
2250
|
+
'Applying this CLEARS any existing satisfaction on this criterion — ticker, ' +
|
|
2251
|
+
'column, note and evidence link — because the sentence the evidence referred ' +
|
|
2252
|
+
'to will no longer exist.',
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
const data = await api('PATCH', path, {
|
|
2256
|
+
body: edit,
|
|
2257
|
+
headers: buildMutationHeaders(args),
|
|
2258
|
+
});
|
|
2259
|
+
return ok({ edited: true, criterion: data?.data });
|
|
2260
|
+
}
|
|
2261
|
+
);
|
|
2262
|
+
|
|
2263
|
+
server.tool(
|
|
2264
|
+
'delete_work_item_acceptance_criterion',
|
|
2265
|
+
`PERMANENTLY REMOVE one acceptance criterion from a card, taking its history with it. Reach for this ONLY when a criterion is unsatisfiable BY CONSTRUCTION — when no evidence any reviewer could gather would ever discharge it. That is not a high bar being enforced; it is a card that can never leave its gate, and it costs a full pass every time a lane draws it and discovers why.
|
|
2266
|
+
|
|
2267
|
+
TWO SHAPES QUALIFY, and both were measured on this board:
|
|
2268
|
+
|
|
2269
|
+
GATE-AS-CRITERION. Its subject is the exit condition of the whole gate or the whole board rather than this card's own deliverable — "every criterion on every QA card carries a fresh receipt", "every card in QA2 is verified". A card cannot verify the queue it is standing in, and a criterion requiring a later gate to have run cannot be discharged from the earlier one.
|
|
2270
|
+
|
|
2271
|
+
PRODUCTION-CERTIFICATION. Its \`whereText\` is the production build after protected merge and deploy. Agents never move a card to Production and never certify production — that is the human owner's act by design — so no QA pass can discharge it at any level of effort. The observation is real and belongs in an acceptance note for the owner, not in a row that gates a QA lane.
|
|
2272
|
+
|
|
2273
|
+
WHAT DOES NOT QUALIFY, which is most of what will tempt you: a criterion that is merely hard, currently failing, blocked on a fixture you do not have, or blocked on an environment that is down. Every one of those is a BOUNCE or a BLOCKER — cheap, reversible, and visible to whoever can clear it. Deletion is none of those things. "I could not satisfy it" and "it cannot be satisfied" are different claims, and only the second one is this tool.
|
|
2274
|
+
|
|
2275
|
+
TRY \`edit_work_item_acceptance_criterion\` FIRST. In practice most impossible-looking criteria have real intent and an impossible SCOPE: rewriting one to the part this card actually owns keeps the intent legible to the next reader, where deleting erases the fact that the definition of done was ever larger. Delete when the intent itself does not belong on this card — not when its wording is wrong.
|
|
2276
|
+
|
|
2277
|
+
Say in \`reason\` which of the two shapes it is and what a reviewer would have had to produce, because after this call nothing downstream can reconstruct what was removed.${GOVERNED_NOTE}`,
|
|
2278
|
+
{
|
|
2279
|
+
itemId: z.string().describe('Card id.'),
|
|
2280
|
+
criterionId: z
|
|
2281
|
+
.string()
|
|
2282
|
+
.describe(
|
|
2283
|
+
'Criterion id from list_work_item_acceptance_criteria — the `id`, not the `ordinal`. ' +
|
|
2284
|
+
'Read the card first: ordinals shift as rows are removed, ids do not.'
|
|
2285
|
+
),
|
|
2286
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to delete it.'),
|
|
2287
|
+
approved: z.boolean().optional().describe('Required true for a live write.'),
|
|
2288
|
+
reason: z
|
|
2289
|
+
.string()
|
|
2290
|
+
.optional()
|
|
2291
|
+
.describe(
|
|
2292
|
+
'Required for a live write: which shape of unsatisfiable-by-construction this is ' +
|
|
2293
|
+
'(gate-as-criterion or production-certification) and what a reviewer would have ' +
|
|
2294
|
+
'had to produce. This is the only surviving record of what was removed.'
|
|
2295
|
+
),
|
|
2296
|
+
idempotencyKey: z.string().optional().describe('Required for a live write. Reuse on retry.'),
|
|
2297
|
+
},
|
|
2298
|
+
async (args) => {
|
|
2299
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/acceptance-criteria/${encodeURIComponent(args.criterionId)}`;
|
|
2300
|
+
const preview = validateApiBridgeRequest({
|
|
2301
|
+
method: 'DELETE',
|
|
2302
|
+
path,
|
|
2303
|
+
dryRun: args.dryRun,
|
|
2304
|
+
approved: args.approved,
|
|
2305
|
+
reason: args.reason,
|
|
2306
|
+
idempotencyKey: args.idempotencyKey,
|
|
2307
|
+
grantedScope: getGrantedScope(),
|
|
2308
|
+
});
|
|
2309
|
+
if (preview?.dryRun) {
|
|
2310
|
+
return ok({
|
|
2311
|
+
...preview,
|
|
2312
|
+
wouldDelete: { itemId: args.itemId, criterionId: args.criterionId },
|
|
2313
|
+
warning:
|
|
2314
|
+
"This is irreversible and takes the criterion's history with it. Confirm from " +
|
|
2315
|
+
'list_work_item_acceptance_criteria that this id is the row you mean — ordinals ' +
|
|
2316
|
+
'shift when a row is removed, ids do not.',
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
const data = await api('DELETE', path, { headers: buildMutationHeaders(args) });
|
|
2320
|
+
return ok({ deleted: true, remaining: data?.data });
|
|
2321
|
+
}
|
|
2322
|
+
);
|
|
2323
|
+
|
|
2115
2324
|
server.tool(
|
|
2116
2325
|
'satisfy_work_item_acceptance_criterion',
|
|
2117
2326
|
`Tick one acceptance criterion with the evidence you actually observed.
|
|
@@ -2277,14 +2486,14 @@ Anyone who can see the card may un-tick, including somebody undoing another pers
|
|
|
2277
2486
|
|
|
2278
2487
|
server.tool(
|
|
2279
2488
|
'record_work_item_criterion_engineering_proof',
|
|
2280
|
-
`Record a structured, non-media Engineering verified receipt for one criterion in its current QA dwell. Use this only when the criterion is explicitly routed engineering or both. It records the exact build, environment, outcome, named code/test/configuration/data/runtime references, and the narrowest honest
|
|
2489
|
+
`Record a structured, non-media Engineering verified receipt for one criterion in its current QA dwell. Use this only when the criterion is explicitly routed engineering or both. It records the exact build, environment, outcome, named code/test/configuration/data/runtime references, and the narrowest honest runtime smoke. Local CLI or isolated runtime checks must declare local; they do not establish a staging deployment or a product user journey. It does not upload an image or video and must never be described as a visual or user-journey pass. QA2 requires a different authenticated credential from the passing QA1 engineering receipt.${GOVERNED_NOTE}`,
|
|
2281
2490
|
{
|
|
2282
2491
|
itemId: z.string().describe('Card id.'),
|
|
2283
2492
|
criterionId: z.string().describe('Criterion id, not its display ordinal.'),
|
|
2284
|
-
environment: z.enum(['staging', 'production']),
|
|
2493
|
+
environment: z.enum(['local', 'staging', 'production']).describe('Where the evidence actually ran; local evidence must never be relabelled staging or production.'),
|
|
2285
2494
|
outcome: z.enum(['pass', 'fail']),
|
|
2286
2495
|
summary: z.string().describe('Short result summary; no raw logs or transcript content.'),
|
|
2287
|
-
buildSha: z.string().regex(/^[0-9a-f]{40}$/).describe('Exact
|
|
2496
|
+
buildSha: z.string().regex(/^[0-9a-f]{40}$/).describe('Exact tested 40-character commit SHA; declare deployment only when actually tested there.'),
|
|
2288
2497
|
stagingSmoke: z.string().describe('The narrowest honest staging/runtime smoke performed.'),
|
|
2289
2498
|
proofs: z
|
|
2290
2499
|
.array(
|
|
@@ -2399,7 +2608,11 @@ A FLAG IS NOT A TAG. The tag says how urgent the WORK is; a flag says the CARD i
|
|
|
2399
2608
|
|
|
2400
2609
|
THE REASON IS REQUIRED IN BOTH DIRECTIONS, and it is not the audit reason — it is the text a human reads on the card. Raising says what is wrong; clearing says what resolved it. Anyone may clear a flag, which only works because both directions are recorded as a comment on the card.
|
|
2401
2610
|
|
|
2402
|
-
This is what the ship-the-card skill means by "an unworkable card is a triage problem, not a coding problem": flag it, say the one question that would unblock it, and stop
|
|
2611
|
+
This is what the ship-the-card skill means by "an unworkable card is a triage problem, not a coding problem": flag it, say the one question that would unblock it, and stop.
|
|
2612
|
+
|
|
2613
|
+
A FLAG HANDS THE CARD ON; IT DOES NOT HIDE IT. Raising one leaves the card claimable, so another agent picks it up, reads your reason, and tries — and reflags with what it learned if it fails too. That loop is the point: write the reason for the next agent, not for a log. Say what you tried and what you ruled out, because they will otherwise re-derive it.
|
|
2614
|
+
|
|
2615
|
+
PASS blocksClaim:true ONLY FOR A BLOCKER NO AGENT CAN SATISFY — an owner credential, a vendor consent, a product decision. That takes the card out of every reviewer's queue until somebody clears it by hand, so it is the wrong answer for "the test failed" or "I could not work out why". If another agent could plausibly get further, leave it claimable.${GOVERNED_NOTE}`,
|
|
2403
2616
|
{
|
|
2404
2617
|
itemId: z.string().describe('Card id.'),
|
|
2405
2618
|
flagged: z
|
|
@@ -2412,6 +2625,12 @@ This is what the ship-the-card skill means by "an unworkable card is a triage pr
|
|
|
2412
2625
|
.describe(
|
|
2413
2626
|
'REQUIRED. Raising: what is wrong with the card. Clearing: what resolved it. Shown on the card and appended to its comment thread — write it for the person who will read it, not for a log.'
|
|
2414
2627
|
),
|
|
2628
|
+
blocksClaim: z
|
|
2629
|
+
.boolean()
|
|
2630
|
+
.optional()
|
|
2631
|
+
.describe(
|
|
2632
|
+
'Whether this flag stops anyone picking the card up. OMITTED MEANS FALSE — the card stays claimable and another agent can try, which is what a flag is for. Pass true only for a blocker no agent can satisfy (an owner credential, a vendor consent, a decision); that hides the card until somebody clears the flag by hand. Ignored when clearing — clearing always restores the blocking default.'
|
|
2633
|
+
),
|
|
2415
2634
|
dryRun: z.boolean().optional().describe('Defaults to true. Set false to change the flag.'),
|
|
2416
2635
|
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2417
2636
|
idempotencyKey: z
|
|
@@ -2446,7 +2665,15 @@ This is what the ship-the-card skill means by "an unworkable card is a triage pr
|
|
|
2446
2665
|
if (preview?.dryRun) {
|
|
2447
2666
|
return ok({
|
|
2448
2667
|
...preview,
|
|
2449
|
-
wouldFlag: {
|
|
2668
|
+
wouldFlag: {
|
|
2669
|
+
itemId: args.itemId,
|
|
2670
|
+
flagged: args.flagged,
|
|
2671
|
+
reason: args.reason,
|
|
2672
|
+
// Shown in the rehearsal because it decides whether the card
|
|
2673
|
+
// disappears from every queue, which is the consequence a reader
|
|
2674
|
+
// most needs to see before approving the write.
|
|
2675
|
+
blocksClaim: args.flagged ? (args.blocksClaim ?? false) : true,
|
|
2676
|
+
},
|
|
2450
2677
|
});
|
|
2451
2678
|
}
|
|
2452
2679
|
|
|
@@ -2455,12 +2682,196 @@ This is what the ship-the-card skill means by "an unworkable card is a triage pr
|
|
|
2455
2682
|
flagged: args.flagged,
|
|
2456
2683
|
reason: args.reason,
|
|
2457
2684
|
idempotencyKey: args.idempotencyKey,
|
|
2685
|
+
// Sent only when the caller named it, so an older API that does not
|
|
2686
|
+
// know the field is not handed one, and the server's own default
|
|
2687
|
+
// (blocking) remains the single source of that decision.
|
|
2688
|
+
...(args.blocksClaim === undefined ? {} : { blocksClaim: args.blocksClaim }),
|
|
2458
2689
|
},
|
|
2459
2690
|
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2460
2691
|
});
|
|
2461
2692
|
return ok({ flagged: args.flagged, item: data?.data });
|
|
2462
2693
|
}
|
|
2463
2694
|
);
|
|
2695
|
+
|
|
2696
|
+
// ── Retiring a card ──────────────────────────────────────────────────────
|
|
2697
|
+
//
|
|
2698
|
+
// Three tools rather than one with a `kind` switch, and the split is
|
|
2699
|
+
// deliberate. `delete_` is matched by tool-annotations' DESTRUCTIVE_PATTERN,
|
|
2700
|
+
// so naming the destructive branch as its own tool is what gets a host to
|
|
2701
|
+
// confirmation-gate it. A single `archive_work_item({ kind: 'deleted' })`
|
|
2702
|
+
// would hide the destructive path behind a name classified as safe.
|
|
2703
|
+
|
|
2704
|
+
const RETIRE_NOTE = `WHAT THIS IS NOT. Do not use move_work_item to Backlog or Deep backlog to retire a card. Those are PARKING: work still intended, later — it stays in every count, which is correct for parked work and wrong for work nobody will ever do. Retiring is the other thing, and it is the only one that takes the card out of the numbers.
|
|
2705
|
+
|
|
2706
|
+
NOTHING IS DESTROYED. The card keeps its board, its column, its column history, its acceptance criteria and its QA evidence. It stays readable by get_work_item and it comes back with unarchive_work_item. The exclusion is a read filter, never a row removal — QA receipts bind to those column dwells and deleting them would leave every receipt on the card unexplainable.${GOVERNED_NOTE}`;
|
|
2707
|
+
|
|
2708
|
+
server.tool(
|
|
2709
|
+
'archive_work_item',
|
|
2710
|
+
`Retire a card: done with, wrong, obsolete, or superseded. It leaves every active count — the board's own size, the roll-up, the personal queue, velocity, capacity, the roadmap's card counts, the criterion standings — and stops being offered to a QA lane.
|
|
2711
|
+
|
|
2712
|
+
This is the COMMON case and it is recoverable. Reach for it whenever a card should stop being counted; if you find yourself wanting to hide a card by moving it somewhere nobody looks, this is the tool you actually wanted.
|
|
2713
|
+
|
|
2714
|
+
${RETIRE_NOTE}`,
|
|
2715
|
+
{
|
|
2716
|
+
itemId: z.string().describe('Card id.'),
|
|
2717
|
+
reason: z
|
|
2718
|
+
.string()
|
|
2719
|
+
.describe(
|
|
2720
|
+
'REQUIRED. Why this work will not be done. Shown on the card and appended to its comment thread — write it for the person who finds the card in six months and wonders what happened to it. "obsolete" is not an answer; "superseded by <card>, the API changed under it" is.'
|
|
2721
|
+
),
|
|
2722
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to retire the card.'),
|
|
2723
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2724
|
+
idempotencyKey: z
|
|
2725
|
+
.string()
|
|
2726
|
+
.optional()
|
|
2727
|
+
.describe(
|
|
2728
|
+
'Required for a live change. Reuse the SAME key on retry — retiring also appends a comment.'
|
|
2729
|
+
),
|
|
2730
|
+
},
|
|
2731
|
+
async (args) => {
|
|
2732
|
+
if (!args.reason || !args.reason.trim()) {
|
|
2733
|
+
return ok({
|
|
2734
|
+
error: true,
|
|
2735
|
+
message:
|
|
2736
|
+
'reason is required. A card that disappears from the board with nothing written on it is exactly what archiving exists to replace.',
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/archive`;
|
|
2740
|
+
const preview = validateApiBridgeRequest({
|
|
2741
|
+
method: 'POST',
|
|
2742
|
+
path,
|
|
2743
|
+
dryRun: args.dryRun,
|
|
2744
|
+
approved: args.approved,
|
|
2745
|
+
reason: args.reason,
|
|
2746
|
+
idempotencyKey: args.idempotencyKey,
|
|
2747
|
+
grantedScope: getGrantedScope(),
|
|
2748
|
+
});
|
|
2749
|
+
if (preview?.dryRun) {
|
|
2750
|
+
return ok({
|
|
2751
|
+
...preview,
|
|
2752
|
+
wouldArchive: { itemId: args.itemId, kind: 'archived', reason: args.reason },
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
const data = await api('POST', path, {
|
|
2756
|
+
body: { reason: args.reason, kind: 'archived', idempotencyKey: args.idempotencyKey },
|
|
2757
|
+
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2758
|
+
});
|
|
2759
|
+
return ok({ archived: true, kind: 'archived', item: data?.data });
|
|
2760
|
+
}
|
|
2761
|
+
);
|
|
2762
|
+
|
|
2763
|
+
server.tool(
|
|
2764
|
+
'delete_work_item',
|
|
2765
|
+
`Retire a card as DELETED rather than archived. Rare, and deliberate.
|
|
2766
|
+
|
|
2767
|
+
PREFER archive_work_item. The two do the same thing to the numbers; the difference is what the record says about the decision, and "archived" is the honest word for almost every card. Use this only when the card should never have existed — a duplicate, a mis-filed capture, something created by an integration by mistake.
|
|
2768
|
+
|
|
2769
|
+
IT IS STILL A SOFT DELETE. The row survives and unarchive_work_item brings it back. That is not a loophole: this board is the audit record behind a two-gate QA process, and a hard delete would cascade away the column history that every QA receipt binds to.
|
|
2770
|
+
|
|
2771
|
+
YOU MUST ECHO THE CARD'S EXACT TITLE in confirmTitle. Read the card first with get_work_item; do not guess it from the id or from a list you are half sure of. The echo is the whole safeguard — it is what makes this something you cannot do by accident inside a loop.
|
|
2772
|
+
|
|
2773
|
+
${RETIRE_NOTE}`,
|
|
2774
|
+
{
|
|
2775
|
+
itemId: z.string().describe('Card id.'),
|
|
2776
|
+
confirmTitle: z
|
|
2777
|
+
.string()
|
|
2778
|
+
.describe(
|
|
2779
|
+
"REQUIRED. The card's title, exactly as get_work_item returns it. A mismatch is refused and nothing is written."
|
|
2780
|
+
),
|
|
2781
|
+
reason: z
|
|
2782
|
+
.string()
|
|
2783
|
+
.describe('REQUIRED. Why this card should never have existed.'),
|
|
2784
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to delete the card.'),
|
|
2785
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2786
|
+
idempotencyKey: z.string().optional().describe('Required for a live change.'),
|
|
2787
|
+
},
|
|
2788
|
+
async (args) => {
|
|
2789
|
+
if (!args.reason || !args.reason.trim()) {
|
|
2790
|
+
return ok({ error: true, message: 'reason is required to delete a card.' });
|
|
2791
|
+
}
|
|
2792
|
+
if (!args.confirmTitle || !args.confirmTitle.trim()) {
|
|
2793
|
+
return ok({
|
|
2794
|
+
error: true,
|
|
2795
|
+
message:
|
|
2796
|
+
"confirmTitle is required and must match the card's exact title. Read the card with get_work_item first — the echo is the only thing standing between a deliberate delete and an accidental one.",
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/archive`;
|
|
2800
|
+
const preview = validateApiBridgeRequest({
|
|
2801
|
+
method: 'POST',
|
|
2802
|
+
path,
|
|
2803
|
+
dryRun: args.dryRun,
|
|
2804
|
+
approved: args.approved,
|
|
2805
|
+
reason: args.reason,
|
|
2806
|
+
idempotencyKey: args.idempotencyKey,
|
|
2807
|
+
grantedScope: getGrantedScope(),
|
|
2808
|
+
});
|
|
2809
|
+
if (preview?.dryRun) {
|
|
2810
|
+
return ok({
|
|
2811
|
+
...preview,
|
|
2812
|
+
wouldDelete: {
|
|
2813
|
+
itemId: args.itemId,
|
|
2814
|
+
kind: 'deleted',
|
|
2815
|
+
reason: args.reason,
|
|
2816
|
+
confirmTitle: args.confirmTitle,
|
|
2817
|
+
},
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
const data = await api('POST', path, {
|
|
2821
|
+
body: {
|
|
2822
|
+
reason: args.reason,
|
|
2823
|
+
kind: 'deleted',
|
|
2824
|
+
confirmTitle: args.confirmTitle,
|
|
2825
|
+
idempotencyKey: args.idempotencyKey,
|
|
2826
|
+
},
|
|
2827
|
+
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2828
|
+
});
|
|
2829
|
+
return ok({ archived: true, kind: 'deleted', item: data?.data });
|
|
2830
|
+
}
|
|
2831
|
+
);
|
|
2832
|
+
|
|
2833
|
+
server.tool(
|
|
2834
|
+
'unarchive_work_item',
|
|
2835
|
+
`Bring a retired card back into circulation, whichever verb retired it — archived or deleted. It rejoins every count exactly where it left, in the same column, with its whole history intact.
|
|
2836
|
+
|
|
2837
|
+
As easy as retiring it was, on purpose: a retirement nobody could undo would make a mistaken one permanent, and that is the failure the confirmation on delete_work_item exists to prevent in the first place.
|
|
2838
|
+
|
|
2839
|
+
THE REASON IS REQUIRED HERE TOO — what changed. Anyone can bring a card back, which only works if bringing it back leaves a record.${GOVERNED_NOTE}`,
|
|
2840
|
+
{
|
|
2841
|
+
itemId: z.string().describe('Card id. Find retired cards with the board archived-items read.'),
|
|
2842
|
+
reason: z.string().describe('REQUIRED. What changed — why this work is wanted again.'),
|
|
2843
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to restore the card.'),
|
|
2844
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2845
|
+
idempotencyKey: z.string().optional().describe('Required for a live change.'),
|
|
2846
|
+
},
|
|
2847
|
+
async (args) => {
|
|
2848
|
+
if (!args.reason || !args.reason.trim()) {
|
|
2849
|
+
return ok({
|
|
2850
|
+
error: true,
|
|
2851
|
+
message:
|
|
2852
|
+
'reason is required to restore a card. Anyone can bring a card back, which only works if bringing it back leaves a record.',
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/unarchive`;
|
|
2856
|
+
const preview = validateApiBridgeRequest({
|
|
2857
|
+
method: 'POST',
|
|
2858
|
+
path,
|
|
2859
|
+
dryRun: args.dryRun,
|
|
2860
|
+
approved: args.approved,
|
|
2861
|
+
reason: args.reason,
|
|
2862
|
+
idempotencyKey: args.idempotencyKey,
|
|
2863
|
+
grantedScope: getGrantedScope(),
|
|
2864
|
+
});
|
|
2865
|
+
if (preview?.dryRun) {
|
|
2866
|
+
return ok({ ...preview, wouldUnarchive: { itemId: args.itemId, reason: args.reason } });
|
|
2867
|
+
}
|
|
2868
|
+
const data = await api('POST', path, {
|
|
2869
|
+
body: { reason: args.reason, idempotencyKey: args.idempotencyKey },
|
|
2870
|
+
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2871
|
+
});
|
|
2872
|
+
return ok({ archived: false, item: data?.data });
|
|
2873
|
+
}
|
|
2874
|
+
);
|
|
2464
2875
|
}
|
|
2465
2876
|
|
|
2466
2877
|
/** Tool names registered here, for the tier map and the toolset manifest. */
|
|
@@ -2495,12 +2906,17 @@ export const WORK_BOARD_TOOL_NAMES = [
|
|
|
2495
2906
|
'create_work_item',
|
|
2496
2907
|
'add_work_item_acceptance_criterion',
|
|
2497
2908
|
'classify_work_item_acceptance_criterion',
|
|
2909
|
+
'edit_work_item_acceptance_criterion',
|
|
2910
|
+
'delete_work_item_acceptance_criterion',
|
|
2498
2911
|
'satisfy_work_item_acceptance_criterion',
|
|
2499
2912
|
'record_work_item_criterion_engineering_proof',
|
|
2500
2913
|
'unsatisfy_work_item_acceptance_criterion',
|
|
2501
2914
|
'get_work_item_comments',
|
|
2502
2915
|
'comment_on_work_item',
|
|
2503
2916
|
'flag_work_item',
|
|
2917
|
+
'archive_work_item',
|
|
2918
|
+
'delete_work_item',
|
|
2919
|
+
'unarchive_work_item',
|
|
2504
2920
|
'block_work_item',
|
|
2505
2921
|
'unblock_work_item',
|
|
2506
2922
|
];
|