@thehammer/danx-dashboard-mcp 0.1.95 → 0.1.97
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 +55 -15
- package/dist/handlers.js +34 -29
- package/dist/index.js +25 -12
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -568,14 +568,23 @@ function classifyAdmissionRefusal(refusal) {
|
|
|
568
568
|
}
|
|
569
569
|
return null;
|
|
570
570
|
}
|
|
571
|
-
|
|
571
|
+
// DX-3028 (code-review + architecture-review finding) — typed against
|
|
572
|
+
// `ListenStopReason` (the closed union `scope_narrowed` aside — see that
|
|
573
|
+
// type's own docblock), NOT bare `Set<string>`: a typo'd member here used to
|
|
574
|
+
// have zero compile-time signal, silently falling into the generic
|
|
575
|
+
// `degrade-server-fixable` default below — exactly the kind of drift this
|
|
576
|
+
// safety-critical classification can least afford. `satisfies readonly
|
|
577
|
+
// ListenStopReason[]` on each array is what makes a typo a build error.
|
|
578
|
+
const EXIT_NEVER_RESTART_LIST = [
|
|
572
579
|
"no_connection_record",
|
|
573
580
|
"not_connected",
|
|
574
581
|
"superseded",
|
|
575
582
|
"replaced",
|
|
576
583
|
"session_is_worker",
|
|
577
|
-
]
|
|
578
|
-
const
|
|
584
|
+
];
|
|
585
|
+
const EXIT_NEVER_RESTART = new Set(EXIT_NEVER_RESTART_LIST);
|
|
586
|
+
const DEGRADE_NO_HEARTBEAT_LIST = ["credential_unavailable", "credential_mismatch"];
|
|
587
|
+
const DEGRADE_NO_HEARTBEAT = new Set(DEGRADE_NO_HEARTBEAT_LIST);
|
|
579
588
|
export function degradePolicyFor(reason) {
|
|
580
589
|
if (EXIT_NEVER_RESTART.has(reason))
|
|
581
590
|
return "exit";
|
|
@@ -796,6 +805,14 @@ export async function runBridge(options, deps) {
|
|
|
796
805
|
if (!readyEmitted) {
|
|
797
806
|
readyEmitted = true;
|
|
798
807
|
const inventory = await fetchPlanInventory(options, deps);
|
|
808
|
+
// DX-3028 (AC1, code-review + architecture-review finding) — this is
|
|
809
|
+
// the ONE place `lastInventoryOk` is ever written; the 15s heartbeat
|
|
810
|
+
// reads it on every "streaming" tick (see `heartbeatTick` above). Must
|
|
811
|
+
// agree with the SAME `ready` record's own `degraded` decision below —
|
|
812
|
+
// a heartbeat reporting `inventory:"ok"` while the `ready` record just
|
|
813
|
+
// told the operator `degraded:true` would be exactly the untruthful
|
|
814
|
+
// state AC1 exists to prevent.
|
|
815
|
+
lastInventoryOk = inventory.kind === "ok";
|
|
799
816
|
if (inventory.kind === "ok") {
|
|
800
817
|
deps.write({
|
|
801
818
|
type: "ready",
|
|
@@ -919,6 +936,18 @@ export async function runBridge(options, deps) {
|
|
|
919
936
|
}
|
|
920
937
|
}
|
|
921
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* DX-3028 — the ONE real `scheduleInterval` implementation, shared by
|
|
941
|
+
* `runBridge`'s deps AND `runBridgeCommand`'s own start-time
|
|
942
|
+
* `degrade-no-heartbeat` keep-alive (see the comment at that call site for
|
|
943
|
+
* why the latter needs one too). A single real `setInterval`/`clearInterval`
|
|
944
|
+
* pair, so the two call sites can never drift into two different ideas of
|
|
945
|
+
* what "the real process's interval" means.
|
|
946
|
+
*/
|
|
947
|
+
function realScheduleInterval(tick, ms) {
|
|
948
|
+
const timer = setInterval(tick, ms);
|
|
949
|
+
return { stop: () => clearInterval(timer) };
|
|
950
|
+
}
|
|
922
951
|
/** The bin's `bridge` subcommand, wired to the real process. */
|
|
923
952
|
export async function runBridgeCommand(argv, env = process.env,
|
|
924
953
|
/** Test seam: the home the session's connection record is read from. */
|
|
@@ -950,14 +979,28 @@ resolveFrom = {}) {
|
|
|
950
979
|
process.stderr.write(`${start.reason}: ${start.message}. Fix: ${start.fix}.\n`);
|
|
951
980
|
if (!degraded)
|
|
952
981
|
return 1;
|
|
953
|
-
// DX-3028 (AC2) — `credential_unavailable` /
|
|
954
|
-
// this process has no credential it can
|
|
955
|
-
// (see `settle`'s identical
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
982
|
+
// DX-3028 (AC2, architecture-review finding) — `credential_unavailable` /
|
|
983
|
+
// `credential_mismatch`: this process has no credential it can
|
|
984
|
+
// authenticate a heartbeat with (see `settle`'s identical
|
|
985
|
+
// `degrade-no-heartbeat` branch), so it sends none — but it MUST still
|
|
986
|
+
// genuinely idle until the plugin kills and restarts it fresh on a
|
|
987
|
+
// watched-path change, exactly like the runtime `settle()` path does.
|
|
988
|
+
// The runtime path stays alive because ITS heartbeat `setInterval` (in
|
|
989
|
+
// `runBridge`, started before its mint loop) is already registered with
|
|
990
|
+
// libuv by the time it degrades; THIS path is reached before `runBridge`
|
|
991
|
+
// (and its interval) ever exists, so a bare `return new Promise(() => {})`
|
|
992
|
+
// here registers no handle at all — Node has nothing left pending once
|
|
993
|
+
// the call stack unwinds and the process exits almost immediately,
|
|
994
|
+
// defeating the "idles forever" contract `BridgeOptions.watchedPaths`
|
|
995
|
+
// exists to serve (DX-2953's watchdog only ever gets a chance to act on
|
|
996
|
+
// a process that is actually still running). `realScheduleInterval()`
|
|
997
|
+
// gives it the same kind of keep-alive handle the runtime path gets for
|
|
998
|
+
// free, ticking a no-op (there is nothing truthful to heartbeat here).
|
|
999
|
+
realScheduleInterval(() => { }, HEARTBEAT_INTERVAL_MS);
|
|
1000
|
+
// `return`ed directly (never `await`ed then fallen through) so
|
|
1001
|
+
// TypeScript's definite-assignment check for `options` below sees this
|
|
1002
|
+
// branch as never reaching that line, which is also true at runtime —
|
|
1003
|
+
// this promise never settles.
|
|
961
1004
|
return new Promise(() => {
|
|
962
1005
|
/* deliberately never settles */
|
|
963
1006
|
});
|
|
@@ -969,9 +1012,6 @@ resolveFrom = {}) {
|
|
|
969
1012
|
now: Date.now,
|
|
970
1013
|
readIdleTimeoutMs: READ_IDLE_TIMEOUT_MS,
|
|
971
1014
|
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
|
972
|
-
scheduleInterval:
|
|
973
|
-
const timer = setInterval(tick, ms);
|
|
974
|
-
return { stop: () => clearInterval(timer) };
|
|
975
|
-
},
|
|
1015
|
+
scheduleInterval: realScheduleInterval,
|
|
976
1016
|
});
|
|
977
1017
|
}
|
package/dist/handlers.js
CHANGED
|
@@ -215,6 +215,12 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
215
215
|
type: args.type,
|
|
216
216
|
title: args.title,
|
|
217
217
|
description: args.description,
|
|
218
|
+
// DX-3006 — forwarded UNCONDITIONALLY, alongside the other required
|
|
219
|
+
// fields, never behind an `if (… !== undefined)` like the optional ones
|
|
220
|
+
// below. Zod has already refused a call that omitted it, so there is
|
|
221
|
+
// nothing to coalesce here: whatever the caller decided, including an
|
|
222
|
+
// explicit null, is what the route receives.
|
|
223
|
+
plan: args.plan,
|
|
218
224
|
};
|
|
219
225
|
if (args.summary !== undefined)
|
|
220
226
|
body.summary = args.summary;
|
|
@@ -226,8 +232,8 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
226
232
|
body.effort_level = args.effort_level;
|
|
227
233
|
if (args.list_id !== undefined)
|
|
228
234
|
body.list_id = args.list_id;
|
|
229
|
-
if (args.
|
|
230
|
-
body.
|
|
235
|
+
if (args.quality_gates !== undefined)
|
|
236
|
+
body.quality_gates = args.quality_gates;
|
|
231
237
|
// DX-1895 (explicit-only) — forward the caller's explicit auto-triage
|
|
232
238
|
// decision; the server stamps false when absent (never auto-triaged).
|
|
233
239
|
// Per-child flags ride along inside phase_children entries verbatim.
|
|
@@ -677,42 +683,41 @@ export async function issueSolution(client, args) {
|
|
|
677
683
|
}
|
|
678
684
|
}
|
|
679
685
|
/**
|
|
680
|
-
*
|
|
681
|
-
* 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
|
|
682
688
|
* dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY
|
|
683
|
-
* post-create path to
|
|
684
|
-
*
|
|
685
|
-
* 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.
|
|
686
691
|
*
|
|
687
692
|
* `gate` is a registry name (`plan-dependency` | `plan-architecture` |
|
|
688
693
|
* `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality`).
|
|
689
|
-
* 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`).
|
|
690
696
|
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
* here launches the gate when the board state is `required` OR `optional`;
|
|
697
|
-
* it is inert ONLY when the board state is `disabled`. Source of truth:
|
|
698
|
-
* `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.
|
|
699
702
|
*
|
|
700
|
-
*
|
|
701
|
-
* `
|
|
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.
|
|
702
707
|
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* tell a fully-honored write from one the board's `required`/`disabled`
|
|
712
|
-
* 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.
|
|
713
716
|
*/
|
|
714
717
|
export async function issueQualityGate(client, args) {
|
|
715
|
-
const body = {
|
|
718
|
+
const body = { action: args.action };
|
|
719
|
+
if (args.note !== undefined)
|
|
720
|
+
body.note = args.note;
|
|
716
721
|
if (args.effort_level !== undefined)
|
|
717
722
|
body.effort_level = args.effort_level;
|
|
718
723
|
return client.request({
|
package/dist/index.js
CHANGED
|
@@ -362,24 +362,32 @@ server.tool("issue_get",
|
|
|
362
362
|
...boardField,
|
|
363
363
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
364
364
|
// ---------------- issue_create ----------------
|
|
365
|
-
server.tool("issue_create", '
|
|
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. `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.', {
|
|
366
367
|
type: z.enum(ISSUE_TYPES),
|
|
367
368
|
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
368
369
|
summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
|
|
369
370
|
description: z.string().describe(DESCRIPTION_DESCRIBE),
|
|
371
|
+
// DX-3006 — REQUIRED and nullable, deliberately. `.nullable()` WITHOUT
|
|
372
|
+
// `.optional()` is the whole point: the caller must supply the key, and
|
|
373
|
+
// `null` is a legitimate answer it has to give on purpose. Adding
|
|
374
|
+
// `.optional()` here silently restores the hole this field closes.
|
|
375
|
+
plan: z
|
|
376
|
+
.literal("mine")
|
|
377
|
+
.nullable()
|
|
378
|
+
.describe('"mine" = attach to the plan THIS session is connected to; null = deliberately no plan. REQUIRED — decide per card. A plan id is not accepted: a card is only ever created onto your own connected plan, so there is no id to name.'),
|
|
370
379
|
parent_id: z.string().nullable().optional(),
|
|
371
380
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
372
381
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
373
382
|
list_id: z.string().min(1).nullable().optional(),
|
|
374
|
-
|
|
383
|
+
quality_gates: z
|
|
375
384
|
.array(z.object({
|
|
376
385
|
gate: z.string().min(1),
|
|
377
|
-
|
|
378
|
-
note: z.string(),
|
|
386
|
+
note: z.string().optional(),
|
|
379
387
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
380
388
|
}))
|
|
381
389
|
.optional()
|
|
382
|
-
.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."),
|
|
383
391
|
phase_children: z
|
|
384
392
|
.array(z.object({
|
|
385
393
|
type: z.enum(NON_EPIC_TYPES),
|
|
@@ -392,15 +400,14 @@ server.tool("issue_create", 'Create a card via POST /api/issues. Board-scoped; s
|
|
|
392
400
|
description: z.string().describe(DESCRIPTION_DESCRIBE),
|
|
393
401
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
394
402
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
395
|
-
|
|
403
|
+
quality_gates: z
|
|
396
404
|
.array(z.object({
|
|
397
405
|
gate: z.string().min(1),
|
|
398
|
-
|
|
399
|
-
note: z.string(),
|
|
406
|
+
note: z.string().optional(),
|
|
400
407
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
401
408
|
}))
|
|
402
409
|
.optional()
|
|
403
|
-
.describe("Same as the root
|
|
410
|
+
.describe("Same as the root quality_gates, resolved against THIS child's type."),
|
|
404
411
|
triage_enabled: z
|
|
405
412
|
.boolean()
|
|
406
413
|
.optional()
|
|
@@ -593,7 +600,7 @@ server.tool("issue_retire_branch", "Mark a card's own `card/<id>` branch RETIRED
|
|
|
593
600
|
...boardField,
|
|
594
601
|
}, async (args) => jsonResult(await issueRetireBranch(client, args)));
|
|
595
602
|
// ---------------- issue_quality_gate ----------------
|
|
596
|
-
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`.", {
|
|
597
604
|
id: z.string().min(1),
|
|
598
605
|
gate: z.enum([
|
|
599
606
|
"plan-dependency",
|
|
@@ -603,12 +610,18 @@ server.tool("issue_quality_gate", "Set one card's per-gate `required` flag via P
|
|
|
603
610
|
"code-architecture",
|
|
604
611
|
"code-quality",
|
|
605
612
|
]),
|
|
606
|
-
|
|
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."),
|
|
607
620
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
608
621
|
...boardField,
|
|
609
622
|
}, async (args) => jsonResult(await issueQualityGate(client, args)));
|
|
610
623
|
// ---------------- issue_quality_gate_verdict ----------------
|
|
611
|
-
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`.", {
|
|
612
625
|
id: z.string().min(1),
|
|
613
626
|
gate: z.enum([
|
|
614
627
|
"plan-dependency",
|
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.97",
|
|
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",
|