@thehammer/danx-dashboard-mcp 0.1.98 → 0.1.101
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/dist/bridge.js +93 -5
- package/dist/handlers.js +28 -29
- package/dist/index.js +16 -12
- package/dist/listen.js +5 -0
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -184,6 +184,27 @@ export const MAX_REFUSED_TICKETS_IN_A_ROW = 2;
|
|
|
184
184
|
export const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
185
185
|
/** DX-3028 (AC2) — the server-fixable degraded reasons' own periodic retry cadence. */
|
|
186
186
|
export const DEGRADED_RETRY_INTERVAL_MS = 5 * 60_000;
|
|
187
|
+
/**
|
|
188
|
+
* DX-3099 — how many consecutive 15s heartbeat POSTs may fail to LAND
|
|
189
|
+
* (network error, timeout, 5xx, 408/429 — never an ordinary 401/403 answer,
|
|
190
|
+
* which proves the pipe itself still works) while this process still
|
|
191
|
+
* believes it is `"streaming"`, before that counts as evidence the current
|
|
192
|
+
* stream connection is dead without having errored — the exact "alive but
|
|
193
|
+
* not reporting" signature the investigation found: the SSE connection can
|
|
194
|
+
* read as established at the OS level and never trip `listen.ts`'s own
|
|
195
|
+
* read-idle timeout, while every outbound heartbeat silently never lands
|
|
196
|
+
* (the send call's own result used to be discarded entirely — see the
|
|
197
|
+
* card). `3 * HEARTBEAT_INTERVAL_MS` = 45s, deliberately UNDER
|
|
198
|
+
* `SESSION_LIVENESS_WINDOW_MS` (60s, `plan-session-heartbeat.ts`) so this
|
|
199
|
+
* process self-heals before, or right around, the moment the dashboard
|
|
200
|
+
* would itself start reporting the session `unattached` — never a shorter
|
|
201
|
+
* freshness window (that constant is untouched) and never a blind
|
|
202
|
+
* "restart more eagerly" (no external watchdog, no timer-driven kill): this
|
|
203
|
+
* fires only on repeated, OBSERVED delivery failure, self-diagnosed by the
|
|
204
|
+
* SAME process that is already streaming. Card: DX-3099 (AC3 forbids both
|
|
205
|
+
* of the obvious wrong fixes this constant is not).
|
|
206
|
+
*/
|
|
207
|
+
export const HEARTBEAT_SILENT_FAILURE_THRESHOLD = 3;
|
|
187
208
|
const TRANSIENT_CLIENT_STATUSES = new Set([408, 429]);
|
|
188
209
|
const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${BRIDGE_SUBCOMMAND} ` +
|
|
189
210
|
`[--resume-ids <event-id>[,<event-id>...]]`;
|
|
@@ -616,13 +637,18 @@ async function sendHeartbeat(options, deps, connection) {
|
|
|
616
637
|
body.reason = connection.reason;
|
|
617
638
|
if (connection.inventory !== undefined)
|
|
618
639
|
body.inventory = connection.inventory;
|
|
619
|
-
await request(options, deps, "heartbeat", `${options.dashboardUrl}${HEARTBEAT_PATH}`, {
|
|
640
|
+
const outcome = await request(options, deps, "heartbeat", `${options.dashboardUrl}${HEARTBEAT_PATH}`, {
|
|
620
641
|
method: "POST",
|
|
621
642
|
body: JSON.stringify({ connection: body }),
|
|
622
643
|
});
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
644
|
+
// DX-3099 — this is now the ONE place a heartbeat's own outcome is
|
|
645
|
+
// inspected. `request()` still never throws, and a 401/403 is still an
|
|
646
|
+
// ordinary answer this function does not retry for (the credential is
|
|
647
|
+
// whatever it is; there is nothing to redo about THIS request) — only a
|
|
648
|
+
// "transient" outcome (network error, timeout, 5xx, 408/429) means the
|
|
649
|
+
// request never landed at all, which is exactly the signal the silent-
|
|
650
|
+
// failure counter in `runBridge` below needs. Still never throws.
|
|
651
|
+
return outcome.kind === "answer";
|
|
626
652
|
}
|
|
627
653
|
/**
|
|
628
654
|
* Mint, stream, re-mint — until a terminal outcome, which it writes as the one
|
|
@@ -642,6 +668,16 @@ export async function runBridge(options, deps) {
|
|
|
642
668
|
let stoppedReason = null;
|
|
643
669
|
let heartbeatSuppressed = false;
|
|
644
670
|
let lastInventoryOk = true;
|
|
671
|
+
// DX-3099 — consecutive heartbeat POSTs that failed to LAND while this
|
|
672
|
+
// process still believed `connectionState === "streaming"`. Reset to 0 the
|
|
673
|
+
// moment a heartbeat lands, or the moment state leaves "streaming" for any
|
|
674
|
+
// other reason (a fresh connect cycle earns a clean slate — see the `for
|
|
675
|
+
// (;;)` loop below). `forceReconnect` is the CURRENT connectOnce attempt's
|
|
676
|
+
// own abort, supplied fresh by `listen.ts` on every attempt via
|
|
677
|
+
// `onConnectionAttempt` below; calling it after that attempt has already
|
|
678
|
+
// ended is a harmless no-op.
|
|
679
|
+
let heartbeatFailureStreak = 0;
|
|
680
|
+
let forceReconnect = null;
|
|
645
681
|
const heartbeatTick = () => {
|
|
646
682
|
if (heartbeatSuppressed)
|
|
647
683
|
return;
|
|
@@ -649,7 +685,48 @@ export async function runBridge(options, deps) {
|
|
|
649
685
|
? { state: "streaming", inventory: lastInventoryOk ? "ok" : "unavailable" }
|
|
650
686
|
: connectionState === "stopped"
|
|
651
687
|
? { state: "stopped", reason: stoppedReason ?? undefined }
|
|
652
|
-
: { state: "reconnecting" })
|
|
688
|
+
: { state: "reconnecting" }).then((landed) => {
|
|
689
|
+
// DX-3099 — only counts while THIS process still believes it is
|
|
690
|
+
// streaming: that is the exact desync the card found (the client
|
|
691
|
+
// believes it is fine; the dashboard has nothing recent for it). A
|
|
692
|
+
// failure while reconnecting/degraded is not new information — the
|
|
693
|
+
// existing mint-backoff / degrade-retry machinery already owns
|
|
694
|
+
// recovery there.
|
|
695
|
+
if (connectionState !== "streaming" || landed) {
|
|
696
|
+
heartbeatFailureStreak = 0;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
heartbeatFailureStreak += 1;
|
|
700
|
+
if (heartbeatFailureStreak < HEARTBEAT_SILENT_FAILURE_THRESHOLD)
|
|
701
|
+
return;
|
|
702
|
+
heartbeatFailureStreak = 0;
|
|
703
|
+
// AC4 — the session finds out, rather than continuing to believe it is
|
|
704
|
+
// connected: an ordinary event line, delivered immediately (`origin:
|
|
705
|
+
// null` skips D4's agent/machine hold — see `routeEvent`'s docblock),
|
|
706
|
+
// which the plugin already relays into the session's inbox unchanged
|
|
707
|
+
// (see this module's own docblock).
|
|
708
|
+
routeEvent({
|
|
709
|
+
type: "event",
|
|
710
|
+
id: null,
|
|
711
|
+
text: oneLine(`${DIGEST_PREFIX} this session's listener stopped reaching the dashboard for ` +
|
|
712
|
+
`${Math.round((HEARTBEAT_SILENT_FAILURE_THRESHOLD * HEARTBEAT_INTERVAL_MS) / 1000)}s while it still ` +
|
|
713
|
+
`believed it was streaming — forcing a fresh connection now.`),
|
|
714
|
+
origin: null,
|
|
715
|
+
});
|
|
716
|
+
// AC2 — force the CURRENT connectOnce attempt to end now, rather than
|
|
717
|
+
// wait on listen.ts's own read-idle timeout (which this exact failure
|
|
718
|
+
// mode can outlive indefinitely — see the card: the connection reads
|
|
719
|
+
// as established and never stops producing keep-alive bytes on the
|
|
720
|
+
// read side, only the outbound heartbeat silently never lands). The
|
|
721
|
+
// new attempt reconnects with the SAME ticket; the dashboard's own
|
|
722
|
+
// "ONE OPEN STREAM PER TICKET" rule (`attachListener`,
|
|
723
|
+
// `plan-session-listeners.ts`) then tells the OLD, now-superseded
|
|
724
|
+
// registry entry to end (`"replaced"`) — which is what actually
|
|
725
|
+
// releases the exclusive claim, as a natural side effect of THIS SAME
|
|
726
|
+
// PROCESS reconnecting, never an external watchdog and never a second
|
|
727
|
+
// process racing the first.
|
|
728
|
+
forceReconnect?.();
|
|
729
|
+
});
|
|
653
730
|
};
|
|
654
731
|
const heartbeat = deps.scheduleInterval(heartbeatTick, HEARTBEAT_INTERVAL_MS);
|
|
655
732
|
async function settle(reason, detail, code, extra) {
|
|
@@ -781,6 +858,11 @@ export async function runBridge(options, deps) {
|
|
|
781
858
|
}
|
|
782
859
|
for (;;) {
|
|
783
860
|
connectionState = "reconnecting";
|
|
861
|
+
// DX-3099 — a fresh cycle earns a clean slate: any abort captured from a
|
|
862
|
+
// now-abandoned prior attempt is dropped (calling it would be a harmless
|
|
863
|
+
// no-op anyway) and the failure streak starts over at 0.
|
|
864
|
+
forceReconnect = null;
|
|
865
|
+
heartbeatFailureStreak = 0;
|
|
784
866
|
const minted = await mintTicket(options, deps);
|
|
785
867
|
if (minted.kind === "terminal") {
|
|
786
868
|
const outcome = await settle(minted.reason, minted.detail, 1);
|
|
@@ -868,6 +950,12 @@ export async function runBridge(options, deps) {
|
|
|
868
950
|
onState: (state) => {
|
|
869
951
|
connectionState = state;
|
|
870
952
|
},
|
|
953
|
+
// DX-3099 — captures the CURRENT connectOnce attempt's own abort, so
|
|
954
|
+
// the heartbeat-failure self-heal above always targets exactly the
|
|
955
|
+
// in-flight connection, never a stale one from an earlier attempt.
|
|
956
|
+
onConnectionAttempt: (abort) => {
|
|
957
|
+
forceReconnect = abort;
|
|
958
|
+
},
|
|
871
959
|
});
|
|
872
960
|
const stopped = run.stopped;
|
|
873
961
|
if (stopped === null)
|
package/dist/handlers.js
CHANGED
|
@@ -232,8 +232,8 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
232
232
|
body.effort_level = args.effort_level;
|
|
233
233
|
if (args.list_id !== undefined)
|
|
234
234
|
body.list_id = args.list_id;
|
|
235
|
-
if (args.
|
|
236
|
-
body.
|
|
235
|
+
if (args.quality_gates !== undefined)
|
|
236
|
+
body.quality_gates = args.quality_gates;
|
|
237
237
|
// DX-1895 (explicit-only) — forward the caller's explicit auto-triage
|
|
238
238
|
// decision; the server stamps false when absent (never auto-triaged).
|
|
239
239
|
// Per-child flags ride along inside phase_children entries verbatim.
|
|
@@ -683,42 +683,41 @@ export async function issueSolution(client, args) {
|
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
/**
|
|
686
|
-
*
|
|
687
|
-
* POST /api/issues/:id/quality-gates/:gate {
|
|
686
|
+
* Put ONE quality gate on a card, or take it off, via
|
|
687
|
+
* POST /api/issues/:id/quality-gates/:gate {action} — the same write the
|
|
688
688
|
* dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY
|
|
689
|
-
* post-create path to
|
|
690
|
-
*
|
|
691
|
-
* this tool an agent that created a card cannot turn a gate on afterward.
|
|
689
|
+
* post-create path to change a card's gates: `issue_create` names them at
|
|
690
|
+
* birth via `quality_gates`, and `issue_edit` rejects gate keys.
|
|
692
691
|
*
|
|
693
692
|
* `gate` is a registry name (`plan-dependency` | `plan-architecture` |
|
|
694
693
|
* `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality`).
|
|
695
|
-
* An unknown gate is a 400 from the server (never a silent no-op)
|
|
694
|
+
* An unknown gate is a 400 from the server (never a silent no-op), as is a
|
|
695
|
+
* card whose type is never gated (`Epic` / `Feature` / `Task`).
|
|
696
696
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* here launches the gate when the board state is `required` OR `optional`;
|
|
703
|
-
* it is inert ONLY when the board state is `disabled`. Source of truth:
|
|
704
|
-
* `isGateEffectivelyRequired` in `src/issues/quality-gates/read.ts`.
|
|
697
|
+
* DX-3015 — there is no board tri-state to reason about any more. A gate is on
|
|
698
|
+
* the card or it is not, and every gate on a card is required, so `add` always
|
|
699
|
+
* means "this gate now runs on this card" and `remove` always means it does
|
|
700
|
+
* not. What the board still owns is the DEFAULT SET a NEW card is born with;
|
|
701
|
+
* it has no say over a card that already exists.
|
|
705
702
|
*
|
|
706
|
-
*
|
|
707
|
-
* `
|
|
703
|
+
* Because of that, the response is plain `{issue}`. The old body carried
|
|
704
|
+
* `{applied, effective, reason}` because the per-card flag was only one of
|
|
705
|
+
* three inputs and the board could silently make the write a no-op — with the
|
|
706
|
+
* tri-state gone, the write IS the outcome and those fields would be constants.
|
|
708
707
|
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
* tell a fully-honored write from one the board's `required`/`disabled`
|
|
718
|
-
* state made a complete no-op.
|
|
708
|
+
* `note` records why the gate is on the card; `effort_level` (DX-1760) sets
|
|
709
|
+
* `card_quality_gates.effort_level` (`null` clears a prior override). Both are
|
|
710
|
+
* `add`-only — the server 400s either one alongside `remove`, since a removed
|
|
711
|
+
* gate has no row to carry them.
|
|
712
|
+
*
|
|
713
|
+
* Re-adding a gate the card already carries updates `note`/`effort_level` and
|
|
714
|
+
* leaves any recorded verdict untouched, so `add` is never a way to quietly
|
|
715
|
+
* clear a `fail`. `remove` DOES discard the row and its verdict.
|
|
719
716
|
*/
|
|
720
717
|
export async function issueQualityGate(client, args) {
|
|
721
|
-
const body = {
|
|
718
|
+
const body = { action: args.action };
|
|
719
|
+
if (args.note !== undefined)
|
|
720
|
+
body.note = args.note;
|
|
722
721
|
if (args.effort_level !== undefined)
|
|
723
722
|
body.effort_level = args.effort_level;
|
|
724
723
|
return client.request({
|
package/dist/index.js
CHANGED
|
@@ -363,7 +363,7 @@ server.tool("issue_get",
|
|
|
363
363
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
364
364
|
// ---------------- issue_create ----------------
|
|
365
365
|
server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass "mine" to put the card on THIS session\'s connected plan, or null when it deliberately belongs to no plan. There is no default and no inference — a card that names no plan is one nobody following the work can see, which is why the answer has to be given rather than omitted. "mine" while this session is on no plan is refused (409 session_not_connected) and creates NO card; a plan id is not accepted (a card is only ever created onto your own connected plan). This replaces the plan_add_card follow-up at creation time; plan_add_card remains for putting an EXISTING card on a plan. ' +
|
|
366
|
-
'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic and phases inserted in one transaction; children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, with no follow-up transition. Not valid on Epic; unknown name/id → 400. `
|
|
366
|
+
'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic and phases inserted in one transaction; children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, with no follow-up transition. Not valid on Epic; unknown name/id → 400. `quality_gates` names the gates this card carries BEYOND the board\'s default set for its type — one `{gate, note?}` each. Omit it for just the board defaults; a gate you do not name simply is not on the card (there is no optional gate and nothing fails closed for going unnamed). Add one later with `issue_quality_gate`. ALWAYS pass `triage_enabled` explicitly on the root card and every phase child: true only when it should enter automatic triage/dispatch without human review; absent → false.', {
|
|
367
367
|
type: z.enum(ISSUE_TYPES),
|
|
368
368
|
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
369
369
|
summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
|
|
@@ -380,15 +380,14 @@ server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass
|
|
|
380
380
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
381
381
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
382
382
|
list_id: z.string().min(1).nullable().optional(),
|
|
383
|
-
|
|
383
|
+
quality_gates: z
|
|
384
384
|
.array(z.object({
|
|
385
385
|
gate: z.string().min(1),
|
|
386
|
-
|
|
387
|
-
note: z.string(),
|
|
386
|
+
note: z.string().optional(),
|
|
388
387
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
389
388
|
}))
|
|
390
389
|
.optional()
|
|
391
|
-
.describe("
|
|
390
|
+
.describe("The gates this card carries BEYOND the board's default set for its type — one {gate, note?} each, `note` = why it applies here. Every gate on a card is required; a gate you do not name is not on the card at all (not displayed, not counted, never run), and omitting this field entirely is normal. Optional `effort_level` overrides a `plan-*` gate's reviewer rung."),
|
|
392
391
|
phase_children: z
|
|
393
392
|
.array(z.object({
|
|
394
393
|
type: z.enum(NON_EPIC_TYPES),
|
|
@@ -401,15 +400,14 @@ server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass
|
|
|
401
400
|
description: z.string().describe(DESCRIPTION_DESCRIBE),
|
|
402
401
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
403
402
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
404
|
-
|
|
403
|
+
quality_gates: z
|
|
405
404
|
.array(z.object({
|
|
406
405
|
gate: z.string().min(1),
|
|
407
|
-
|
|
408
|
-
note: z.string(),
|
|
406
|
+
note: z.string().optional(),
|
|
409
407
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
410
408
|
}))
|
|
411
409
|
.optional()
|
|
412
|
-
.describe("Same as the root
|
|
410
|
+
.describe("Same as the root quality_gates, resolved against THIS child's type."),
|
|
413
411
|
triage_enabled: z
|
|
414
412
|
.boolean()
|
|
415
413
|
.optional()
|
|
@@ -602,7 +600,7 @@ server.tool("issue_retire_branch", "Mark a card's own `card/<id>` branch RETIRED
|
|
|
602
600
|
...boardField,
|
|
603
601
|
}, async (args) => jsonResult(await issueRetireBranch(client, args)));
|
|
604
602
|
// ---------------- issue_quality_gate ----------------
|
|
605
|
-
server.tool("issue_quality_gate", "
|
|
603
|
+
server.tool("issue_quality_gate", "Put one quality gate ON a card, or take it OFF, via POST /api/issues/:id/quality-gates/:gate {action} — the only post-create way (issue_create names gates in quality_gates; issue_edit refuses gate keys). A gate is on the card or it does not exist for it; every gate on a card is required, so `add` means it now runs and `remove` means it is gone (not displayed, not counted). Adding a gate at any time is fully supported — that is what this tool is for. PRE `plan-*` gates run before the work dispatch; POST `code-*` gates block complete. Unknown gate → 400; a never-gated card type (Epic/Feature/Task) → 400. `note` (why it applies) and `effort_level` (overrides a `plan-*` gate's reviewer rung; null clears it) are `add`-only — passing either with `remove` → 400. Re-adding a gate the card already has updates note/effort and KEEPS its verdict; `remove` discards the row and any verdict on it. Board-scoped; see `board`.", {
|
|
606
604
|
id: z.string().min(1),
|
|
607
605
|
gate: z.enum([
|
|
608
606
|
"plan-dependency",
|
|
@@ -612,12 +610,18 @@ server.tool("issue_quality_gate", "Set one card's per-gate `required` flag via P
|
|
|
612
610
|
"code-architecture",
|
|
613
611
|
"code-quality",
|
|
614
612
|
]),
|
|
615
|
-
|
|
613
|
+
action: z
|
|
614
|
+
.enum(["add", "remove"])
|
|
615
|
+
.describe("add = put this gate on the card; remove = take it off."),
|
|
616
|
+
note: z
|
|
617
|
+
.string()
|
|
618
|
+
.optional()
|
|
619
|
+
.describe("Why this gate applies to this card. `add` only."),
|
|
616
620
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
617
621
|
...boardField,
|
|
618
622
|
}, async (args) => jsonResult(await issueQualityGate(client, args)));
|
|
619
623
|
// ---------------- issue_quality_gate_verdict ----------------
|
|
620
|
-
server.tool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one
|
|
624
|
+
server.tool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one puts a gate ON or OFF the card (does this gate apply at all), THIS one records the VERDICT (did it pass) — POST vs PATCH on the same resource, neither substitutes for the other. **Use this to close out a card you picked up with `issue_transition pickup {manual:true}`** (DX-946 operator-session self-pickup): `issue_transition complete` REFUSES 409 (`failed_gate: \"quality_gate_post\"`, `failed_post_gates[]`) while any required POST gate (`code-quality` / `code-test-quality` / `code-architecture`) is not `pass`, so without a verdict a manually-claimed card can never reach Done — it strands In Progress and its `conflict_on` / `waiting_on` edges then stall OTHER cards' dispatch. `status`: `pass` | `fail` | `pending` (revert a prior verdict, clears the message). `message` is the accountability record for the override, REQUIRED at >= 20 characters for `pass`/`fail` (shorter → 400), ignored for `pending`. Record the REAL reviewer finding, not a rubber stamp — a human-attributed override, stamped with the operator actor, standing in for a reviewer dispatch. A manual verdict is a PURE row write: no side effects — a manual `fail` never blocks the card and a manual `pass` never releases a dispatch. Unknown gate → 400; bad status → 400; unknown card → 404. Board-scoped; see `board`.", {
|
|
621
625
|
id: z.string().min(1),
|
|
622
626
|
gate: z.enum([
|
|
623
627
|
"plan-dependency",
|
package/dist/listen.js
CHANGED
|
@@ -499,6 +499,11 @@ export async function runListener(options, deps) {
|
|
|
499
499
|
};
|
|
500
500
|
const connectOnce = async () => {
|
|
501
501
|
const controller = new AbortController();
|
|
502
|
+
// DX-3099 — hand THIS attempt's own abort to the caller before doing
|
|
503
|
+
// anything else, so a force-reconnect requested at any point during the
|
|
504
|
+
// attempt (including before the fetch even starts) targets exactly this
|
|
505
|
+
// controller, never a stale one from a previous attempt.
|
|
506
|
+
deps.onConnectionAttempt?.(() => controller.abort());
|
|
502
507
|
let idle;
|
|
503
508
|
const armIdle = () => {
|
|
504
509
|
clearTimeout(idle);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.101",
|
|
4
4
|
"description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|