@adrata/adrata-mcp 1.0.19 → 1.0.40
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 +3 -0
- package/access/tiers.js +27 -0
- package/api-bridge.js +6 -0
- package/package.json +3 -3
- package/security.js +167 -14
- package/server.js +24 -1
- package/server.json +2 -2
- package/skills/qa-the-card/SKILL.md +16 -3
- package/skills/ship-the-card/SKILL.md +48 -39
- package/tool-annotations.js +17 -0
- package/tools/provisioning/onboarding-tools.js +730 -0
- package/tools/source-control/connection-tools.js +117 -1
- package/tools/work-board-tools.js +449 -39
- package/tools/work-hub/criteria-quality.js +254 -5
- package/tools/work-hub/field-changes.js +25 -0
- package/toolsets/prospecting.js +194 -3
- package/toolsets/revenue/always-loaded.js +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerWorkItemFieldChanges } from './work-hub/field-changes.js';
|
|
1
2
|
import { auditWorkHubBoards, deliveryContradictions } from './work-hub/audit.js';
|
|
2
3
|
import { describeCriteriaQuality, inspectCriteria } from './work-hub/criteria-quality.js';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
@@ -184,6 +185,88 @@ export function describeMissingAcceptanceCriteria(criteria) {
|
|
|
184
185
|
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.';
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Why a card with no product will not reach a QA lane, or `undefined` if it has
|
|
190
|
+
* one.
|
|
191
|
+
*
|
|
192
|
+
* `product` reads like an optional label and is not one. Every QA drain claims
|
|
193
|
+
* for ONE exact product, and the dispatcher selects on `i.product = $4` — SQL
|
|
194
|
+
* equality against NULL is not true, so an unclassified card is not ranked last
|
|
195
|
+
* or shown late, it is absent from every product queue at once while each of
|
|
196
|
+
* them reports itself empty. Measured 2026-09-08 on the Adrata board: 3 of 7
|
|
197
|
+
* cards in Staging QA2 carried no product, a product-scoped census returned 0
|
|
198
|
+
* workable, and those three were the only workable cards there.
|
|
199
|
+
*
|
|
200
|
+
* Advisory rather than a refusal, on the same argument `describeMissingBody`
|
|
201
|
+
* makes: a refused capture is how work stops reaching the board at all. The
|
|
202
|
+
* preview is where the author is already deciding, and this is the cheapest
|
|
203
|
+
* moment the omission is ever visible.
|
|
204
|
+
*/
|
|
205
|
+
/**
|
|
206
|
+
* The population, the exclusions, and which exclusions are themselves suspect.
|
|
207
|
+
*
|
|
208
|
+
* Returned whether or not a card was claimed. Shaped as counts plus the cards
|
|
209
|
+
* behind them, because a total is the last thing to trust and a reader who
|
|
210
|
+
* doubts the summary needs the rows.
|
|
211
|
+
*/
|
|
212
|
+
export function describeEmptyQueue(census) {
|
|
213
|
+
const finished = census?.skippedFinished ?? [];
|
|
214
|
+
const unrouted = census?.unroutedProduct ?? [];
|
|
215
|
+
return {
|
|
216
|
+
considered: census?.considered ?? 0,
|
|
217
|
+
truncated: census?.truncated === true,
|
|
218
|
+
skippedFinishedCount: finished.length,
|
|
219
|
+
skippedFinished: finished,
|
|
220
|
+
unroutedProductCount: unrouted.length,
|
|
221
|
+
unroutedProduct: unrouted,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What a lane should DO about an empty answer.
|
|
227
|
+
*
|
|
228
|
+
* Ordered by what changes the lane's next action, most actionable first. The
|
|
229
|
+
* unrouted clause comes before the flat "nothing remains" sentence on purpose:
|
|
230
|
+
* a gate holding unclassified cards is NOT empty, and saying it is would be the
|
|
231
|
+
* same false all-clear in new words.
|
|
232
|
+
*/
|
|
233
|
+
export function emptyQueueNote(product, qaGate, skippedBlocked, census) {
|
|
234
|
+
const parts = [];
|
|
235
|
+
const finished = census?.skippedFinished?.length ?? 0;
|
|
236
|
+
const unrouted = census?.unroutedProduct?.length ?? 0;
|
|
237
|
+
const considered = census?.considered ?? 0;
|
|
238
|
+
|
|
239
|
+
if (skippedBlocked?.length) {
|
|
240
|
+
parts.push(
|
|
241
|
+
`${skippedBlocked.length} card(s) in this gate are blocked by another card. Do not re-claim — read the blockers in skippedBlocked.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (finished) {
|
|
245
|
+
parts.push(
|
|
246
|
+
`${finished} card(s) were skipped because every acceptance criterion is already verified in this dwell. They are DONE, not pending — do not re-run a pass on them; they are listed in census.skippedFinished for whoever advances them.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (unrouted) {
|
|
250
|
+
parts.push(
|
|
251
|
+
`${unrouted} card(s) stand in ${qaGate} with NO product set. They are invisible to every product-scoped lane at once, including this one — this gate is unrouted, not empty. Set a product on them (they are listed in census.unroutedProduct) rather than concluding there is no work here.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (census?.truncated) {
|
|
255
|
+
parts.push(
|
|
256
|
+
'The candidate window was full, so the ranking saw only its first page; this census describes that page, not the whole gate.'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (!parts.length) {
|
|
260
|
+
return `No eligible unclaimed ${product} ${qaGate} pass remains on any visible board: ${considered} candidate(s) were considered and none was excluded. Pick another lane.`;
|
|
261
|
+
}
|
|
262
|
+
return `No workable ${product} ${qaGate} pass was handed out. ${considered} candidate(s) considered. ${parts.join(' ')}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function describeMissingProduct(product) {
|
|
266
|
+
if (String(product ?? '').trim()) return undefined;
|
|
267
|
+
return 'This card would arrive with no `product`. QA lanes drain ONE exact product at a time and select on an exact match, so a card without one is invisible to every lane at once rather than merely deprioritised — and each of those queues reports itself empty. Set `product` unless you intend this card to be worked by hand.';
|
|
268
|
+
}
|
|
269
|
+
|
|
187
270
|
/**
|
|
188
271
|
* Make the created card's own criteria counter agree with the criteria returned
|
|
189
272
|
* beside it.
|
|
@@ -267,6 +350,7 @@ export function registerWorkBoardTools(
|
|
|
267
350
|
uploadQaEvidenceFile = uploadLocalQaEvidenceFile,
|
|
268
351
|
}
|
|
269
352
|
) {
|
|
353
|
+
registerWorkItemFieldChanges(server, { z, api, ok });
|
|
270
354
|
const GOVERNED_NOTE =
|
|
271
355
|
' Governed write: previews by default. A live write requires dryRun:false plus approved:true, a reason, and an idempotencyKey (reuse the SAME key on retry — a duplicate move would read as the card having bounced between columns).';
|
|
272
356
|
// Capability material belongs to this MCP process, not to the model transcript.
|
|
@@ -274,7 +358,7 @@ export function registerWorkBoardTools(
|
|
|
274
358
|
// public id and the handler adds the three private headers at the last hop.
|
|
275
359
|
const workerLeaseCapabilities = new Map();
|
|
276
360
|
|
|
277
|
-
function rememberWorkerLease(itemId, grant, claimIdempotencyKey) {
|
|
361
|
+
function rememberWorkerLease(itemId, grant, claimIdempotencyKey, workerLabel) {
|
|
278
362
|
if (!grant?.leaseId || !grant?.leaseToken || !Number.isInteger(grant?.fencingToken)) {
|
|
279
363
|
throw new Error('The API returned an incomplete worker-lease grant; no capability was stored.');
|
|
280
364
|
}
|
|
@@ -283,18 +367,32 @@ export function registerWorkBoardTools(
|
|
|
283
367
|
leaseToken: grant.leaseToken,
|
|
284
368
|
fencingToken: grant.fencingToken,
|
|
285
369
|
claimIdempotencyKey,
|
|
370
|
+
workerLabel,
|
|
286
371
|
});
|
|
287
372
|
const { leaseToken: _secret, fencingToken: _fence, qaRequirements, currentCriteria, ...lease } =
|
|
288
373
|
grant;
|
|
289
374
|
return { lease, qaRequirements, currentCriteria };
|
|
290
375
|
}
|
|
291
376
|
|
|
292
|
-
|
|
377
|
+
// ONE PASS PER WORKER, NOT PER PROCESS. A fleet runs many QA lanes through a
|
|
378
|
+
// single connector, so scanning every capability in the map made lane A's
|
|
379
|
+
// card refuse lane B — naming a card lane B had never seen and telling it to
|
|
380
|
+
// "transition or release" a peer's pass. That serialises the parallel QA
|
|
381
|
+
// drain the atomic claim exists to make safe, and it does so silently: the
|
|
382
|
+
// refusal reads exactly like ordinary contention for one card.
|
|
383
|
+
//
|
|
384
|
+
// The worker is what the guard was always about, and `workerLabel` is how a
|
|
385
|
+
// worker names itself. A capability stored without one cannot be attributed,
|
|
386
|
+
// so it still conflicts — an unattributable hold fails toward refusing, which
|
|
387
|
+
// costs a retry, rather than toward a double hold, which costs a lost lease.
|
|
388
|
+
function conflictingHeldCard(itemId, claimIdempotencyKey, workerLabel) {
|
|
293
389
|
for (const [heldItemId, capability] of workerLeaseCapabilities) {
|
|
294
390
|
const sameCard = itemId != null && heldItemId === itemId;
|
|
295
391
|
const sameClaim =
|
|
296
392
|
claimIdempotencyKey != null && capability.claimIdempotencyKey === claimIdempotencyKey;
|
|
297
|
-
|
|
393
|
+
const sameWorker =
|
|
394
|
+
capability.workerLabel == null || capability.workerLabel === workerLabel;
|
|
395
|
+
if (!sameCard && !sameClaim && sameWorker) return heldItemId;
|
|
298
396
|
}
|
|
299
397
|
return null;
|
|
300
398
|
}
|
|
@@ -511,7 +609,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
511
609
|
idempotencyKey: z.string().optional(),
|
|
512
610
|
},
|
|
513
611
|
async (args) => {
|
|
514
|
-
const heldItemId = conflictingHeldCard(args.itemId, args.idempotencyKey);
|
|
612
|
+
const heldItemId = conflictingHeldCard(args.itemId, args.idempotencyKey, args.workerLabel);
|
|
515
613
|
if (heldItemId) {
|
|
516
614
|
return ok({
|
|
517
615
|
error: true,
|
|
@@ -547,7 +645,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
547
645
|
body,
|
|
548
646
|
headers: buildMutationHeaders(args),
|
|
549
647
|
});
|
|
550
|
-
const visible = rememberWorkerLease(args.itemId, data?.data, args.idempotencyKey);
|
|
648
|
+
const visible = rememberWorkerLease(args.itemId, data?.data, args.idempotencyKey, args.workerLabel);
|
|
551
649
|
return ok({ claimed: true, ...visible });
|
|
552
650
|
}
|
|
553
651
|
);
|
|
@@ -567,7 +665,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
567
665
|
idempotencyKey: z.string().optional(),
|
|
568
666
|
},
|
|
569
667
|
async (args) => {
|
|
570
|
-
const heldItemId = conflictingHeldCard(null, args.idempotencyKey);
|
|
668
|
+
const heldItemId = conflictingHeldCard(null, args.idempotencyKey, args.workerLabel);
|
|
571
669
|
if (heldItemId) {
|
|
572
670
|
return ok({
|
|
573
671
|
error: true,
|
|
@@ -611,24 +709,42 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
611
709
|
const outcome = data?.data ?? {};
|
|
612
710
|
const selected = outcome.claimed;
|
|
613
711
|
const skippedBlocked = outcome.skippedBlocked ?? [];
|
|
712
|
+
const census = outcome.census ?? {};
|
|
614
713
|
if (!selected) {
|
|
615
|
-
// The empty-handed case is
|
|
616
|
-
// them differently: an empty gate means pick another lane
|
|
617
|
-
// fully-blocked one means go and look at the blockers
|
|
714
|
+
// The empty-handed case is FOUR different situations and a lane acts on
|
|
715
|
+
// them differently: an empty gate means pick another lane; a
|
|
716
|
+
// fully-blocked one means go and look at the blockers; a gate whose
|
|
717
|
+
// work is all verified means the drain is finished; and cards with no
|
|
718
|
+
// product mean the queue is not empty at all, it is unrouted.
|
|
719
|
+
//
|
|
720
|
+
// So this answer shows its working rather than asserting a conclusion.
|
|
721
|
+
// A signal that can tell a worker to STOP has to be contradictable —
|
|
722
|
+
// this factory has already believed "there is genuinely nothing to pick
|
|
723
|
+
// up" once, from a count that could not be wrong out loud, while 211 of
|
|
724
|
+
// 212 claims sat on cards that had left the column.
|
|
618
725
|
return ok({
|
|
619
726
|
claimed: false,
|
|
620
727
|
qaGate: args.qaGate,
|
|
621
728
|
product: args.product,
|
|
622
729
|
skippedBlocked,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
: `No eligible unclaimed ${args.product} ${args.qaGate} pass remains on any visible board.`,
|
|
730
|
+
census: describeEmptyQueue(census),
|
|
731
|
+
note: emptyQueueNote(args.product, args.qaGate, skippedBlocked, census),
|
|
626
732
|
});
|
|
627
733
|
}
|
|
628
|
-
const visible = rememberWorkerLease(
|
|
734
|
+
const visible = rememberWorkerLease(
|
|
735
|
+
selected.workItemId,
|
|
736
|
+
selected.grant,
|
|
737
|
+
args.idempotencyKey,
|
|
738
|
+
args.workerLabel
|
|
739
|
+
);
|
|
629
740
|
return ok({
|
|
630
741
|
claimed: true,
|
|
631
742
|
skippedBlocked,
|
|
743
|
+
// Reported on the winning path as well. Three finished cards and two
|
|
744
|
+
// unrouted ones in this gate are a board finding whoever is reading,
|
|
745
|
+
// and a lane that only sees them on the empty pass learns about them
|
|
746
|
+
// last -- by which point it has no reason to look.
|
|
747
|
+
census: describeEmptyQueue(census),
|
|
632
748
|
workItemId: selected.workItemId,
|
|
633
749
|
boardId: selected.boardId,
|
|
634
750
|
boardName: selected.boardName,
|
|
@@ -727,10 +843,11 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
727
843
|
|
|
728
844
|
server.tool(
|
|
729
845
|
'record_work_item_qa_failure_and_release',
|
|
730
|
-
`Durably record why the current QA pass cannot proceed, then safely release the process-private lease without moving the card. A failed criterion is flagged first, un-ticked, and left on the same QA card
|
|
846
|
+
`Durably record why the current QA pass cannot proceed, then safely release the process-private lease without moving the card. A failed criterion is flagged first, un-ticked, and left on the same QA card for the next worker to diagnose, repair, deploy and independently retest. Ordinary failure and dependency handoffs remain claimable. Set blocksClaim:true only for an explicit prerequisite no agent can satisfy, such as owner consent or a vendor credential. Both paths release the lease only after the failure is durably recorded; a claimable handoff is not a QA pass. This is one governed recovery recipe, not a new bug card and not a QA bounce.${GOVERNED_NOTE}`,
|
|
731
847
|
{
|
|
732
848
|
itemId: z.string().describe('Card currently claimed by this MCP worker.'),
|
|
733
849
|
disposition: z.enum(['failed_criterion', 'blocked_dependency']),
|
|
850
|
+
blocksClaim: z.boolean().optional().default(false).describe('Stop pickup only for an explicit prerequisite no agent can satisfy. Ordinary failed work remains claimable for repair and retest.'),
|
|
734
851
|
criterionId: z.string().optional().describe('Required for failed_criterion; the exact acceptance criterion contradicted.'),
|
|
735
852
|
details: z.string().describe('Concrete observed failure or blocker and the recovery needed. Stored on the card.'),
|
|
736
853
|
dryRun: z.boolean().optional().default(true),
|
|
@@ -748,7 +865,7 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
748
865
|
const preview = validateApiBridgeRequest({
|
|
749
866
|
method: 'POST',
|
|
750
867
|
path: `/api/v1/work-items/${encodeURIComponent(args.itemId)}/comments`,
|
|
751
|
-
body: { disposition: args.disposition, criterionId: args.criterionId, details: args.details },
|
|
868
|
+
body: { disposition: args.disposition, criterionId: args.criterionId, details: args.details, blocksClaim: args.blocksClaim === true },
|
|
752
869
|
dryRun: args.dryRun, approved: args.approved, reason: args.reason,
|
|
753
870
|
idempotencyKey: args.idempotencyKey, grantedScope: getGrantedScope(),
|
|
754
871
|
});
|
|
@@ -760,10 +877,11 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
760
877
|
disposition: args.disposition,
|
|
761
878
|
criterionId: args.criterionId ?? null,
|
|
762
879
|
details: args.details,
|
|
880
|
+
blocksClaim: args.blocksClaim === true,
|
|
763
881
|
},
|
|
764
|
-
note: args.
|
|
765
|
-
? 'The card will
|
|
766
|
-
: 'The card will
|
|
882
|
+
note: args.blocksClaim === true
|
|
883
|
+
? 'The card will be flagged in place and withheld from pickup until the explicit prerequisite is resolved.'
|
|
884
|
+
: 'The card will stay in place, retain its failure reason, and remain claimable for repair and retest after this pass is released.',
|
|
767
885
|
});
|
|
768
886
|
}
|
|
769
887
|
const leaseHeaders = workerLeaseHeaders(args.itemId);
|
|
@@ -776,11 +894,11 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
776
894
|
const flagReason = args.disposition === 'failed_criterion'
|
|
777
895
|
? `QA criterion failure (${args.criterionId}): ${args.details.trim()} Waiting for a fixed deployed build before requeue.`
|
|
778
896
|
: args.details.trim();
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
await api('POST', `/api/v1/work-items/${encodeURIComponent(args.itemId)}/flag`, {
|
|
783
|
-
body: { flagged: true, reason: flagReason, idempotencyKey: recordKey },
|
|
897
|
+
// Preserve the contradiction before un-ticking or releasing. Ordinary
|
|
898
|
+
// failures remain claimable so another worker can repair them; only an
|
|
899
|
+
// explicitly requested hold blocks pickup. Child writes are idempotent.
|
|
900
|
+
const recordedFlag = await api('POST', `/api/v1/work-items/${encodeURIComponent(args.itemId)}/flag`, {
|
|
901
|
+
body: { flagged: true, reason: flagReason, blocksClaim: args.blocksClaim === true, idempotencyKey: recordKey },
|
|
784
902
|
headers: mutationHeadersForItem({ ...args, idempotencyKey: recordKey }, args.itemId),
|
|
785
903
|
});
|
|
786
904
|
if (args.disposition === 'failed_criterion') {
|
|
@@ -798,8 +916,10 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
798
916
|
recorded: true,
|
|
799
917
|
disposition: args.disposition,
|
|
800
918
|
requeued: false,
|
|
801
|
-
blocked: true,
|
|
919
|
+
blocked: args.blocksClaim === true,
|
|
920
|
+
claimable: args.blocksClaim !== true,
|
|
802
921
|
waitingForFix: args.disposition === 'failed_criterion',
|
|
922
|
+
flagGeneration: recordedFlag?.data?.recordedFlagGeneration ?? null,
|
|
803
923
|
releaseKind,
|
|
804
924
|
...release,
|
|
805
925
|
});
|
|
@@ -808,9 +928,10 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
808
928
|
|
|
809
929
|
server.tool(
|
|
810
930
|
'requeue_work_item_qa_after_fix',
|
|
811
|
-
`Clear a failed QA card’s waiting-for-fix flag only after a different exact build has been deployed and its relationship to the failed build has been checked. This does not claim the card; it
|
|
931
|
+
`Clear a failed QA card’s waiting-for-fix flag only after a different exact build has been deployed and its relationship to the failed build has been checked. This does not claim or certify the card; it clears the resolved hold or failure handoff before a fresh independent QA pass. The clear reason durably records both SHAs and the fix/deployment reference. The worker must verify ancestry in source control before calling this tool; this endpoint rejects the unchanged SHA but does not pretend to be a Git ancestry oracle.${GOVERNED_NOTE}`,
|
|
812
932
|
{
|
|
813
933
|
itemId: z.string().describe('Flagged QA card left in place by record_work_item_qa_failure_and_release.'),
|
|
934
|
+
repairedFlagGeneration: z.string().min(1).describe('The flag.generation captured for the failure actually repaired. Never substitute a newer flag generation without resolving that failure.'),
|
|
814
935
|
failedBuildSha: z.string().length(40).describe('Exact lowercase 40-character SHA of the build that failed.'),
|
|
815
936
|
deployedBuildSha: z.string().length(40).describe('Exact lowercase 40-character SHA of the newer deployed descendant build.'),
|
|
816
937
|
details: z.string().describe('How ancestry and deployment were verified, including the fix PR or deployment reference.'),
|
|
@@ -830,6 +951,9 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
830
951
|
if (!args.details?.trim()) {
|
|
831
952
|
return ok({ error: true, message: 'details must name the ancestry check and deployment/fix reference.' });
|
|
832
953
|
}
|
|
954
|
+
if (!args.repairedFlagGeneration?.trim()) {
|
|
955
|
+
return ok({ error: true, message: 'repairedFlagGeneration is required. Read and resolve the current failure before retrying; never substitute a newer generation for an older repair.' });
|
|
956
|
+
}
|
|
833
957
|
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/flag`;
|
|
834
958
|
const clearReason = `QA requeue: deployed ${args.deployedBuildSha} after failed ${args.failedBuildSha}. ${args.details.trim()}`;
|
|
835
959
|
const preview = validateApiBridgeRequest({
|
|
@@ -837,13 +961,13 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
837
961
|
reason: args.reason, idempotencyKey: args.idempotencyKey, grantedScope: getGrantedScope(),
|
|
838
962
|
});
|
|
839
963
|
if (preview.dryRun) {
|
|
840
|
-
return ok({ ...preview, wouldRequeue: { itemId: args.itemId, failedBuildSha: args.failedBuildSha, deployedBuildSha: args.deployedBuildSha } });
|
|
964
|
+
return ok({ ...preview, wouldRequeue: { itemId: args.itemId, failedBuildSha: args.failedBuildSha, deployedBuildSha: args.deployedBuildSha, repairedFlagGeneration: args.repairedFlagGeneration } });
|
|
841
965
|
}
|
|
842
966
|
const data = await api('POST', path, {
|
|
843
|
-
body: { flagged: false, reason: clearReason, idempotencyKey: args.idempotencyKey },
|
|
967
|
+
body: { flagged: false, reason: clearReason, expectedFlagGeneration: args.repairedFlagGeneration, idempotencyKey: args.idempotencyKey },
|
|
844
968
|
headers: buildMutationHeaders(args),
|
|
845
969
|
});
|
|
846
|
-
return ok({ requeued: true, item: data?.data, failedBuildSha: args.failedBuildSha, deployedBuildSha: args.deployedBuildSha });
|
|
970
|
+
return ok({ requeued: true, item: data?.data, failedBuildSha: args.failedBuildSha, deployedBuildSha: args.deployedBuildSha, repairedFlagGeneration: args.repairedFlagGeneration });
|
|
847
971
|
}
|
|
848
972
|
);
|
|
849
973
|
|
|
@@ -1253,6 +1377,41 @@ Start here for "what should I work on". With includeUnassigned it also returns t
|
|
|
1253
1377
|
}
|
|
1254
1378
|
);
|
|
1255
1379
|
|
|
1380
|
+
server.tool(
|
|
1381
|
+
'preview_work_item_acceptance_reaffirmation',
|
|
1382
|
+
'Read the exact current criterion definitions and preserved independent QA evidence for a legacy human-accepted Ready to Ship card. Returns a content digest for a separate explicit owner decision. Original acceptance scope remains unknown; this never repairs missing QA.',
|
|
1383
|
+
{ itemId: z.string() },
|
|
1384
|
+
async (args) => {
|
|
1385
|
+
const data = await api('GET', `/api/v1/work-items/${encodeURIComponent(args.itemId)}/acceptance-reaffirmation`);
|
|
1386
|
+
return ok(data?.data);
|
|
1387
|
+
}
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
server.tool(
|
|
1391
|
+
'reaffirm_work_item_current_acceptance',
|
|
1392
|
+
`Record a separate owner approval of the exact CURRENT scope and existing QA evidence from preview_work_item_acceptance_reaffirmation. Only the authenticated delegated OAuth owner can do this. The original legacy acceptance receipt stays unchanged and its original scope stays unknown. This neither verifies criteria nor fills missing QA receipts. Use only on the owner's explicit instruction after reviewing the preview and limitation.${GOVERNED_NOTE}`,
|
|
1393
|
+
{
|
|
1394
|
+
itemId: z.string(), historyId: z.string(), originalReceiptId: z.string(),
|
|
1395
|
+
contentSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1396
|
+
acknowledgeOriginalScopeUnknown: z.literal(true),
|
|
1397
|
+
receivingBuildSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
1398
|
+
expiresAt: z.number().int(),
|
|
1399
|
+
dryRun: z.boolean().optional(), approved: z.boolean().optional(),
|
|
1400
|
+
reason: z.string().optional(), idempotencyKey: z.string().optional(),
|
|
1401
|
+
},
|
|
1402
|
+
async (args) => {
|
|
1403
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/acceptance-reaffirmation`;
|
|
1404
|
+
const preview = validateApiBridgeRequest({ method: 'POST', path, dryRun: args.dryRun,
|
|
1405
|
+
approved: args.approved, reason: args.reason, idempotencyKey: args.idempotencyKey,
|
|
1406
|
+
grantedScope: getGrantedScope() });
|
|
1407
|
+
const { dryRun: _dryRun, ...body } = args;
|
|
1408
|
+
if (preview?.dryRun) return ok({ ...preview, wouldReaffirm: body,
|
|
1409
|
+
limitation: 'Original legacy acceptance scope remains unknown. This is a separate current acceptance.' });
|
|
1410
|
+
const data = await api('POST', path, { body, headers: buildMutationHeaders(args) });
|
|
1411
|
+
return ok({ reaffirmed: true, receipt: data?.data });
|
|
1412
|
+
}
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1256
1415
|
server.tool(
|
|
1257
1416
|
'move_work_item',
|
|
1258
1417
|
`Move a card to another column on the same board — and, outside QA, use claim:true to pick it up in the same action. The server does this in ONE transaction: it closes the card's open dwell, appends the transition to the history, records you as the handler of the pass the card is now on, and updates the card. Dropping a card into the column it is already in is a REORDER and deliberately does not restamp the stage timer.
|
|
@@ -1285,6 +1444,20 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1285
1444
|
),
|
|
1286
1445
|
dryRun: z.boolean().optional().describe('Defaults to true. Set false for a live move.'),
|
|
1287
1446
|
approved: z.boolean().optional().describe('Required true for a live move.'),
|
|
1447
|
+
presenceAttestation: z.string().optional().describe('Single-use human-minted presence token. This remains separate from owner delegation.'),
|
|
1448
|
+
ownerApproval: z.object({
|
|
1449
|
+
approved: z.literal(true),
|
|
1450
|
+
itemId: z.string(),
|
|
1451
|
+
toColumnId: z.string(),
|
|
1452
|
+
idempotencyKey: z.string(),
|
|
1453
|
+
acceptanceReceiptId: z.string(),
|
|
1454
|
+
reaffirmationReceiptId: z.string().optional().describe('Separate current acceptance receipt for legacy original scope; never a backfilled original receipt.'),
|
|
1455
|
+
reviewedBuildSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
1456
|
+
receivingBuildSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
1457
|
+
expiresAt: z.number().int(),
|
|
1458
|
+
}).strict().optional().describe(
|
|
1459
|
+
'Explicit owner-delegated Production request. Use only after the owner approves this exact card, destination, accepted QA2 closing receipt and reviewed delivery build. Expires within five minutes. Server requires the authenticated owner’s delegated OAuth credential and preserves species agent. This is delegated authority, not cryptographic proof of human presence. receivingBuildSha is the live API build; reviewedBuildSha is the separately reviewed component build and must match receipt.commitSha.'
|
|
1460
|
+
),
|
|
1288
1461
|
reason: z
|
|
1289
1462
|
.string()
|
|
1290
1463
|
.optional()
|
|
@@ -1407,6 +1580,8 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1407
1580
|
claim: args.claim === true,
|
|
1408
1581
|
force: args.force === true,
|
|
1409
1582
|
receipt: args.receipt !== undefined,
|
|
1583
|
+
ownerApproval: args.ownerApproval,
|
|
1584
|
+
presenceAttestation: args.presenceAttestation !== undefined,
|
|
1410
1585
|
},
|
|
1411
1586
|
wip: projectColumnWip({
|
|
1412
1587
|
limit: target.wipLimit,
|
|
@@ -1474,6 +1649,8 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1474
1649
|
acknowledgeUnmetCriteria:
|
|
1475
1650
|
args.acknowledgeUnmetCriteria === true ? true : undefined,
|
|
1476
1651
|
receipt: args.receipt,
|
|
1652
|
+
ownerApproval: args.ownerApproval,
|
|
1653
|
+
presenceAttestation: args.presenceAttestation,
|
|
1477
1654
|
},
|
|
1478
1655
|
headers: mutationHeadersForItem(args, args.itemId),
|
|
1479
1656
|
});
|
|
@@ -1484,11 +1661,25 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1484
1661
|
return ok({
|
|
1485
1662
|
moved: true,
|
|
1486
1663
|
claimed: args.claim === true,
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1664
|
+
// Read back from the SERVER, never `args.receipt !== undefined`. That
|
|
1665
|
+
// echo reported the request rather than the answer, and the two
|
|
1666
|
+
// disagree in all three directions: `receipt: {}` points at nothing and
|
|
1667
|
+
// the server grades it `server_recorded`; offering no receipt at all
|
|
1668
|
+
// still writes a `server_recorded` row, so `false` was wrong; and a
|
|
1669
|
+
// REORDER writes no receipt whatsoever, because the API guards the
|
|
1670
|
+
// write with `if changed_column` — yet a receipt passed alongside one
|
|
1671
|
+
// still reported `true`.
|
|
1672
|
+
//
|
|
1673
|
+
// Still a BOOLEAN and still never the receipt's contents: repeating a
|
|
1489
1674
|
// session pointer back into a conversation transcript is the leak this
|
|
1490
1675
|
// whole field is shaped to avoid.
|
|
1491
|
-
receiptRecorded:
|
|
1676
|
+
receiptRecorded: data?.receiptEvidenceSource != null,
|
|
1677
|
+
// Whether that receipt points at anything, which is a different
|
|
1678
|
+
// question from whether one exists — and it is the one the gaps query
|
|
1679
|
+
// asks. `attested` only when the caller's references actually carried a
|
|
1680
|
+
// value; `server_recorded` when the server wrote the row on its own
|
|
1681
|
+
// observation; null when no receipt was written for this call.
|
|
1682
|
+
receiptEvidenceSource: data?.receiptEvidenceSource ?? null,
|
|
1492
1683
|
// Read back from the server rather than echoed from the request, because
|
|
1493
1684
|
// the two halves of a claim are decided server-side and an agent that
|
|
1494
1685
|
// assumed "claimed" meant "mine now" would report a QA pick-up as having
|
|
@@ -1605,7 +1796,12 @@ QA PASSES ARE THE EXCEPTION. An executable QA worker must hold the process-priva
|
|
|
1605
1796
|
return ok({
|
|
1606
1797
|
transferred: true,
|
|
1607
1798
|
identityPreserved: true,
|
|
1608
|
-
|
|
1799
|
+
// The server's verdict, not an echo of the request. See the move tool
|
|
1800
|
+
// above for why those differ. A transfer always changes board AND
|
|
1801
|
+
// column, so unlike a move it always writes a receipt — but whether
|
|
1802
|
+
// that receipt points at anything is still the server's call.
|
|
1803
|
+
receiptRecorded: data?.receiptEvidenceSource != null,
|
|
1804
|
+
receiptEvidenceSource: data?.receiptEvidenceSource ?? null,
|
|
1609
1805
|
item: data?.data,
|
|
1610
1806
|
});
|
|
1611
1807
|
}
|
|
@@ -1941,6 +2137,9 @@ YOU DO NOT NEED TO SAY WHO IS CREATING IT. The card records its creator from you
|
|
|
1941
2137
|
// capture is how work stops reaching the board at all.
|
|
1942
2138
|
note: describeMissingBody(args.body),
|
|
1943
2139
|
readinessNote: describeMissingAcceptanceCriteria(args.acceptanceCriteria),
|
|
2140
|
+
// A missing product is not a missing label: it is the difference
|
|
2141
|
+
// between a card a QA lane can be handed and one no lane can see.
|
|
2142
|
+
routingNote: describeMissingProduct(args.product),
|
|
1944
2143
|
// Advisory, and only ever present when it has something to say. The
|
|
1945
2144
|
// preview is where an author is already deciding whether to approve,
|
|
1946
2145
|
// and a criterion that cannot fail is cheapest to fix before the card
|
|
@@ -2564,25 +2763,34 @@ TO @-MENTION SOMEBODY, write the token \`<@userId>\` in the body — the id come
|
|
|
2564
2763
|
|
|
2565
2764
|
server.tool(
|
|
2566
2765
|
'flag_work_item',
|
|
2567
|
-
`Flag
|
|
2766
|
+
`Flag unresolved work for the next repair and retest, or clear a flag once its reason is resolved. Ordinary failures remain visible and claimable.
|
|
2767
|
+
|
|
2768
|
+
A FLAG IS NOT A TAG. The tag says how urgent the work is; a flag records an unresolved problem. If a card is merely important, use set_work_item_tag.
|
|
2568
2769
|
|
|
2569
|
-
|
|
2770
|
+
THE REASON IS REQUIRED IN BOTH DIRECTIONS, and it is not the audit reason — it is the text read on the card. Raising says what failed, what you tried, and what remains to check; clearing says what resolved it. Agents may clear a resolved flag without a human handoff. Both directions append a comment, preserving the original failure and its resolution.
|
|
2570
2771
|
|
|
2571
|
-
THE
|
|
2772
|
+
A FLAG HANDS THE CARD ON; IT DOES NOT HIDE IT. A failed test or an unsuccessful first repair is not an external hold. Leave it claimable so the next agent can investigate, repair, and independently retest. Clearing a flag does not certify acceptance criteria or replace either QA gate.
|
|
2572
2773
|
|
|
2573
|
-
|
|
2774
|
+
PASS blocksClaim:true ONLY FOR AN EXPLICIT PREREQUISITE AGENTS CANNOT SATISFY — owner authorization, vendor consent, or a product decision. The hold remains until that prerequisite is satisfied and the flag is cleared with a resolution reason. Do not turn an ordinary test failure into an owner decision.${GOVERNED_NOTE}`,
|
|
2574
2775
|
{
|
|
2575
2776
|
itemId: z.string().describe('Card id.'),
|
|
2576
2777
|
flagged: z
|
|
2577
2778
|
.boolean()
|
|
2578
2779
|
.describe(
|
|
2579
|
-
'true raises the flag; false clears it. Raising an already-flagged card replaces the reason.'
|
|
2780
|
+
'true raises the flag; false clears it. Raising an already-flagged card replaces the reason, and that is an EDIT rather than a second flag: the card carries a lifetime count of how many times it has been flagged (drawn on the board as a stack of one, two, three, or three-plus pennants), and only an unflagged card becoming flagged increments it. So narrowing a live flag costs nothing, and a card showing four pennants really has been through the loop four times.'
|
|
2580
2781
|
),
|
|
2581
2782
|
reason: z
|
|
2582
2783
|
.string()
|
|
2583
2784
|
.describe(
|
|
2584
2785
|
'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.'
|
|
2585
2786
|
),
|
|
2787
|
+
expectedFlagGeneration: z.string().optional().describe('Required when clearing: flag.generation captured for the failure actually resolved. A stale generation is refused; inspect and resolve the newer flag before another clear.'),
|
|
2788
|
+
blocksClaim: z
|
|
2789
|
+
.boolean()
|
|
2790
|
+
.optional()
|
|
2791
|
+
.describe(
|
|
2792
|
+
'Whether this flag prevents claims. OMITTED MEANS FALSE: ordinary failed work remains claimable for repair and independent retest. Pass true only for an explicit prerequisite agents cannot satisfy, such as owner authorization or vendor consent. Clear after that prerequisite is satisfied, recording the resolution. Ignored when clearing.'
|
|
2793
|
+
),
|
|
2586
2794
|
dryRun: z.boolean().optional().describe('Defaults to true. Set false to change the flag.'),
|
|
2587
2795
|
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2588
2796
|
idempotencyKey: z
|
|
@@ -2597,10 +2805,13 @@ This is what the ship-the-card skill means by "an unworkable card is a triage pr
|
|
|
2597
2805
|
return ok({
|
|
2598
2806
|
error: true,
|
|
2599
2807
|
message:
|
|
2600
|
-
'reason is required to flag or to clear.
|
|
2808
|
+
'reason is required to flag or to clear. Record the unresolved problem when raising and what resolved it when clearing; both become card history.',
|
|
2601
2809
|
});
|
|
2602
2810
|
}
|
|
2603
2811
|
|
|
2812
|
+
if (!args.flagged && !args.expectedFlagGeneration?.trim()) {
|
|
2813
|
+
return ok({ error: true, message: 'Clearing requires expectedFlagGeneration from the failure actually resolved. Read and resolve the current flag before retrying.' });
|
|
2814
|
+
}
|
|
2604
2815
|
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/flag`;
|
|
2605
2816
|
const preview = validateApiBridgeRequest({
|
|
2606
2817
|
method: 'POST',
|
|
@@ -2617,25 +2828,220 @@ This is what the ship-the-card skill means by "an unworkable card is a triage pr
|
|
|
2617
2828
|
if (preview?.dryRun) {
|
|
2618
2829
|
return ok({
|
|
2619
2830
|
...preview,
|
|
2620
|
-
wouldFlag: {
|
|
2831
|
+
wouldFlag: {
|
|
2832
|
+
itemId: args.itemId,
|
|
2833
|
+
flagged: args.flagged,
|
|
2834
|
+
expectedFlagGeneration: args.expectedFlagGeneration,
|
|
2835
|
+
reason: args.reason,
|
|
2836
|
+
// Shown in the rehearsal because it decides whether the card
|
|
2837
|
+
// disappears from every queue, which is the consequence a reader
|
|
2838
|
+
// most needs to see before approving the write.
|
|
2839
|
+
blocksClaim: args.flagged ? (args.blocksClaim ?? false) : true,
|
|
2840
|
+
},
|
|
2621
2841
|
});
|
|
2622
2842
|
}
|
|
2623
2843
|
|
|
2624
2844
|
const data = await api('POST', path, {
|
|
2625
2845
|
body: {
|
|
2626
2846
|
flagged: args.flagged,
|
|
2847
|
+
expectedFlagGeneration: args.expectedFlagGeneration,
|
|
2627
2848
|
reason: args.reason,
|
|
2628
2849
|
idempotencyKey: args.idempotencyKey,
|
|
2850
|
+
// Match the preview explicitly: ordinary failures remain claimable.
|
|
2851
|
+
// Clearing ignores this field on the server.
|
|
2852
|
+
blocksClaim: args.flagged ? (args.blocksClaim ?? false) : true,
|
|
2629
2853
|
},
|
|
2630
2854
|
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2631
2855
|
});
|
|
2632
2856
|
return ok({ flagged: args.flagged, item: data?.data });
|
|
2633
2857
|
}
|
|
2634
2858
|
);
|
|
2859
|
+
|
|
2860
|
+
// ── Retiring a card ──────────────────────────────────────────────────────
|
|
2861
|
+
//
|
|
2862
|
+
// Three tools rather than one with a `kind` switch, and the split is
|
|
2863
|
+
// deliberate. `delete_` is matched by tool-annotations' DESTRUCTIVE_PATTERN,
|
|
2864
|
+
// so naming the destructive branch as its own tool is what gets a host to
|
|
2865
|
+
// confirmation-gate it. A single `archive_work_item({ kind: 'deleted' })`
|
|
2866
|
+
// would hide the destructive path behind a name classified as safe.
|
|
2867
|
+
|
|
2868
|
+
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.
|
|
2869
|
+
|
|
2870
|
+
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}`;
|
|
2871
|
+
|
|
2872
|
+
server.tool(
|
|
2873
|
+
'archive_work_item',
|
|
2874
|
+
`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.
|
|
2875
|
+
|
|
2876
|
+
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.
|
|
2877
|
+
|
|
2878
|
+
${RETIRE_NOTE}`,
|
|
2879
|
+
{
|
|
2880
|
+
itemId: z.string().describe('Card id.'),
|
|
2881
|
+
reason: z
|
|
2882
|
+
.string()
|
|
2883
|
+
.describe(
|
|
2884
|
+
'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.'
|
|
2885
|
+
),
|
|
2886
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to retire the card.'),
|
|
2887
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2888
|
+
idempotencyKey: z
|
|
2889
|
+
.string()
|
|
2890
|
+
.optional()
|
|
2891
|
+
.describe(
|
|
2892
|
+
'Required for a live change. Reuse the SAME key on retry — retiring also appends a comment.'
|
|
2893
|
+
),
|
|
2894
|
+
},
|
|
2895
|
+
async (args) => {
|
|
2896
|
+
if (!args.reason || !args.reason.trim()) {
|
|
2897
|
+
return ok({
|
|
2898
|
+
error: true,
|
|
2899
|
+
message:
|
|
2900
|
+
'reason is required. A card that disappears from the board with nothing written on it is exactly what archiving exists to replace.',
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/archive`;
|
|
2904
|
+
const preview = validateApiBridgeRequest({
|
|
2905
|
+
method: 'POST',
|
|
2906
|
+
path,
|
|
2907
|
+
dryRun: args.dryRun,
|
|
2908
|
+
approved: args.approved,
|
|
2909
|
+
reason: args.reason,
|
|
2910
|
+
idempotencyKey: args.idempotencyKey,
|
|
2911
|
+
grantedScope: getGrantedScope(),
|
|
2912
|
+
});
|
|
2913
|
+
if (preview?.dryRun) {
|
|
2914
|
+
return ok({
|
|
2915
|
+
...preview,
|
|
2916
|
+
wouldArchive: { itemId: args.itemId, kind: 'archived', reason: args.reason },
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
const data = await api('POST', path, {
|
|
2920
|
+
body: { reason: args.reason, kind: 'archived', idempotencyKey: args.idempotencyKey },
|
|
2921
|
+
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2922
|
+
});
|
|
2923
|
+
return ok({ archived: true, kind: 'archived', item: data?.data });
|
|
2924
|
+
}
|
|
2925
|
+
);
|
|
2926
|
+
|
|
2927
|
+
server.tool(
|
|
2928
|
+
'delete_work_item',
|
|
2929
|
+
`Retire a card as DELETED rather than archived. Rare, and deliberate.
|
|
2930
|
+
|
|
2931
|
+
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.
|
|
2932
|
+
|
|
2933
|
+
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.
|
|
2934
|
+
|
|
2935
|
+
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.
|
|
2936
|
+
|
|
2937
|
+
${RETIRE_NOTE}`,
|
|
2938
|
+
{
|
|
2939
|
+
itemId: z.string().describe('Card id.'),
|
|
2940
|
+
confirmTitle: z
|
|
2941
|
+
.string()
|
|
2942
|
+
.describe(
|
|
2943
|
+
"REQUIRED. The card's title, exactly as get_work_item returns it. A mismatch is refused and nothing is written."
|
|
2944
|
+
),
|
|
2945
|
+
reason: z
|
|
2946
|
+
.string()
|
|
2947
|
+
.describe('REQUIRED. Why this card should never have existed.'),
|
|
2948
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to delete the card.'),
|
|
2949
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
2950
|
+
idempotencyKey: z.string().optional().describe('Required for a live change.'),
|
|
2951
|
+
},
|
|
2952
|
+
async (args) => {
|
|
2953
|
+
if (!args.reason || !args.reason.trim()) {
|
|
2954
|
+
return ok({ error: true, message: 'reason is required to delete a card.' });
|
|
2955
|
+
}
|
|
2956
|
+
if (!args.confirmTitle || !args.confirmTitle.trim()) {
|
|
2957
|
+
return ok({
|
|
2958
|
+
error: true,
|
|
2959
|
+
message:
|
|
2960
|
+
"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.",
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/archive`;
|
|
2964
|
+
const preview = validateApiBridgeRequest({
|
|
2965
|
+
method: 'POST',
|
|
2966
|
+
path,
|
|
2967
|
+
dryRun: args.dryRun,
|
|
2968
|
+
approved: args.approved,
|
|
2969
|
+
reason: args.reason,
|
|
2970
|
+
idempotencyKey: args.idempotencyKey,
|
|
2971
|
+
grantedScope: getGrantedScope(),
|
|
2972
|
+
});
|
|
2973
|
+
if (preview?.dryRun) {
|
|
2974
|
+
return ok({
|
|
2975
|
+
...preview,
|
|
2976
|
+
wouldDelete: {
|
|
2977
|
+
itemId: args.itemId,
|
|
2978
|
+
kind: 'deleted',
|
|
2979
|
+
reason: args.reason,
|
|
2980
|
+
confirmTitle: args.confirmTitle,
|
|
2981
|
+
},
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
const data = await api('POST', path, {
|
|
2985
|
+
body: {
|
|
2986
|
+
reason: args.reason,
|
|
2987
|
+
kind: 'deleted',
|
|
2988
|
+
confirmTitle: args.confirmTitle,
|
|
2989
|
+
idempotencyKey: args.idempotencyKey,
|
|
2990
|
+
},
|
|
2991
|
+
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
2992
|
+
});
|
|
2993
|
+
return ok({ archived: true, kind: 'deleted', item: data?.data });
|
|
2994
|
+
}
|
|
2995
|
+
);
|
|
2996
|
+
|
|
2997
|
+
server.tool(
|
|
2998
|
+
'unarchive_work_item',
|
|
2999
|
+
`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.
|
|
3000
|
+
|
|
3001
|
+
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.
|
|
3002
|
+
|
|
3003
|
+
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}`,
|
|
3004
|
+
{
|
|
3005
|
+
itemId: z.string().describe('Card id. Find retired cards with the board archived-items read.'),
|
|
3006
|
+
reason: z.string().describe('REQUIRED. What changed — why this work is wanted again.'),
|
|
3007
|
+
dryRun: z.boolean().optional().describe('Defaults to true. Set false to restore the card.'),
|
|
3008
|
+
approved: z.boolean().optional().describe('Required true for a live change.'),
|
|
3009
|
+
idempotencyKey: z.string().optional().describe('Required for a live change.'),
|
|
3010
|
+
},
|
|
3011
|
+
async (args) => {
|
|
3012
|
+
if (!args.reason || !args.reason.trim()) {
|
|
3013
|
+
return ok({
|
|
3014
|
+
error: true,
|
|
3015
|
+
message:
|
|
3016
|
+
'reason is required to restore a card. Anyone can bring a card back, which only works if bringing it back leaves a record.',
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
const path = `/api/v1/work-items/${encodeURIComponent(args.itemId)}/unarchive`;
|
|
3020
|
+
const preview = validateApiBridgeRequest({
|
|
3021
|
+
method: 'POST',
|
|
3022
|
+
path,
|
|
3023
|
+
dryRun: args.dryRun,
|
|
3024
|
+
approved: args.approved,
|
|
3025
|
+
reason: args.reason,
|
|
3026
|
+
idempotencyKey: args.idempotencyKey,
|
|
3027
|
+
grantedScope: getGrantedScope(),
|
|
3028
|
+
});
|
|
3029
|
+
if (preview?.dryRun) {
|
|
3030
|
+
return ok({ ...preview, wouldUnarchive: { itemId: args.itemId, reason: args.reason } });
|
|
3031
|
+
}
|
|
3032
|
+
const data = await api('POST', path, {
|
|
3033
|
+
body: { reason: args.reason, idempotencyKey: args.idempotencyKey },
|
|
3034
|
+
headers: buildMutationHeaders({ ...args, reason: args.reason }),
|
|
3035
|
+
});
|
|
3036
|
+
return ok({ archived: false, item: data?.data });
|
|
3037
|
+
}
|
|
3038
|
+
);
|
|
2635
3039
|
}
|
|
2636
3040
|
|
|
2637
3041
|
/** Tool names registered here, for the tier map and the toolset manifest. */
|
|
2638
3042
|
export const WORK_BOARD_TOOL_NAMES = [
|
|
3043
|
+
'preview_work_item_acceptance_reaffirmation',
|
|
3044
|
+
'reaffirm_work_item_current_acceptance',
|
|
2639
3045
|
'list_my_work_items',
|
|
2640
3046
|
'list_work_boards',
|
|
2641
3047
|
'get_work_board',
|
|
@@ -2649,6 +3055,7 @@ export const WORK_BOARD_TOOL_NAMES = [
|
|
|
2649
3055
|
'record_work_item_qa_failure_and_release',
|
|
2650
3056
|
'requeue_work_item_qa_after_fix',
|
|
2651
3057
|
'get_work_item_history',
|
|
3058
|
+
'get_work_item_field_changes',
|
|
2652
3059
|
'get_work_item_delivery_evidence',
|
|
2653
3060
|
'audit_work_hub',
|
|
2654
3061
|
'list_work_item_acceptance_criteria',
|
|
@@ -2674,6 +3081,9 @@ export const WORK_BOARD_TOOL_NAMES = [
|
|
|
2674
3081
|
'get_work_item_comments',
|
|
2675
3082
|
'comment_on_work_item',
|
|
2676
3083
|
'flag_work_item',
|
|
3084
|
+
'archive_work_item',
|
|
3085
|
+
'delete_work_item',
|
|
3086
|
+
'unarchive_work_item',
|
|
2677
3087
|
'block_work_item',
|
|
2678
3088
|
'unblock_work_item',
|
|
2679
3089
|
];
|