@thehammer/danx-dashboard-mcp 0.1.84 → 0.1.86
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/listen.js +116 -1
- package/package.json +1 -1
package/dist/listen.js
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* - `{type:"event", id, text}` — exactly one per event, the moment it arrives.
|
|
12
12
|
* `text` is what the session reads; an event it cannot read still produces one
|
|
13
13
|
* (`could not read event …`), never silence. `id` is `null` only for a frame
|
|
14
|
-
* that carried no usable id
|
|
14
|
+
* that carried no usable id — which includes DX-2885's `nudge` frame
|
|
15
|
+
* (`formatNudgeLine`): it is advisory, re-derived fresh every tick by the
|
|
16
|
+
* dashboard's own keep-alive tick, and never replayed, so it rides this
|
|
17
|
+
* same shape with `id: null` rather than growing a THIRD output variant.
|
|
15
18
|
* - `{type:"stopped", reason, detail, refusal?, boards?}` — once, last. `reason`
|
|
16
19
|
* is the dashboard's own end reason (`superseded`, `replaced`, `revoked`,
|
|
17
20
|
* `scope_narrowed`, `not_connected`), `refused` (the ticket was not
|
|
@@ -122,6 +125,32 @@ export function invalidEventReason(value) {
|
|
|
122
125
|
}
|
|
123
126
|
return null;
|
|
124
127
|
}
|
|
128
|
+
// DX-2935 — the operator retracted a problem's answer with no new one yet.
|
|
129
|
+
case "problem_unanswered": {
|
|
130
|
+
const problem = d.problem;
|
|
131
|
+
if (!isRecord(problem) || typeof problem.id !== "number" || !isCappedText(problem.statement)) {
|
|
132
|
+
return "detail.problem is not {id, statement}";
|
|
133
|
+
}
|
|
134
|
+
return typeof d.decision_id === "number" ? null : "detail.decision_id is not a number";
|
|
135
|
+
}
|
|
136
|
+
// DX-2935 — the operator retracted a problem's answer AND recorded a new
|
|
137
|
+
// one, in one transaction — same either/or solution/freeform shape
|
|
138
|
+
// `solution_answered` carries, plus which decision was retracted.
|
|
139
|
+
case "problem_answer_changed": {
|
|
140
|
+
const problem = d.problem;
|
|
141
|
+
if (!isRecord(problem) || typeof problem.id !== "number" || !isCappedText(problem.statement)) {
|
|
142
|
+
return "detail.problem is not {id, statement}";
|
|
143
|
+
}
|
|
144
|
+
if (typeof d.retracted_decision_id !== "number")
|
|
145
|
+
return "detail.retracted_decision_id is not a number";
|
|
146
|
+
if (typeof d.decision_id !== "number")
|
|
147
|
+
return "detail.decision_id is not a number";
|
|
148
|
+
if (d.solution === null)
|
|
149
|
+
return isCappedText(d.freeform) ? null : "detail.freeform is not capped text";
|
|
150
|
+
if (!isRecord(d.solution) || typeof d.solution.title !== "string")
|
|
151
|
+
return "detail.solution is not {title, note}";
|
|
152
|
+
return d.solution.note === null || isCappedText(d.solution.note) ? null : "detail.solution.note is not capped text";
|
|
153
|
+
}
|
|
125
154
|
case "blocked":
|
|
126
155
|
return isCappedText(d.reason) ? null : "detail.reason is not capped text";
|
|
127
156
|
default:
|
|
@@ -160,6 +189,22 @@ function describe(event) {
|
|
|
160
189
|
const problem = d.problem;
|
|
161
190
|
return `${event.actor} opened a problem: ${quoted(problem.statement)}`;
|
|
162
191
|
}
|
|
192
|
+
case "problem_unanswered": {
|
|
193
|
+
// DX-2935 — MUST read as "a previous answer was withdrawn", never as a
|
|
194
|
+
// brand-new problem: a connected session that already acted on the
|
|
195
|
+
// retracted answer has to stop, not treat this as fresh work to start.
|
|
196
|
+
const problem = d.problem;
|
|
197
|
+
return `${event.actor} retracted the answer to ${quoted(problem.statement)}`;
|
|
198
|
+
}
|
|
199
|
+
case "problem_answer_changed": {
|
|
200
|
+
// DX-2935 — ONE line for the whole retract-and-reanswer (never two
|
|
201
|
+
// separate events), naming the statement so a listener with several
|
|
202
|
+
// open questions knows which one just moved.
|
|
203
|
+
const problem = d.problem;
|
|
204
|
+
const solution = d.solution;
|
|
205
|
+
const now = solution === null ? quoted(d.freeform) : `"${solution.title}"`;
|
|
206
|
+
return `${event.actor} changed the answer to ${quoted(problem.statement)}: now ${now}`;
|
|
207
|
+
}
|
|
163
208
|
case "blocked":
|
|
164
209
|
return `${event.actor} blocked the card: ${quoted(d.reason)}`;
|
|
165
210
|
case "unblocked":
|
|
@@ -177,6 +222,46 @@ export function formatActivityLine(event) {
|
|
|
177
222
|
throw new Error(`malformed activity event: ${invalid}`);
|
|
178
223
|
return oneLine(`[${event.cardId} "${event.cardTitle}" ${event.boardId}] ${describe(event)}`);
|
|
179
224
|
}
|
|
225
|
+
function isNudgeCardRef(value) {
|
|
226
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.title === "string";
|
|
227
|
+
}
|
|
228
|
+
function isNudgeCardRefArray(value) {
|
|
229
|
+
return Array.isArray(value) && value.every(isNudgeCardRef);
|
|
230
|
+
}
|
|
231
|
+
/** Why a nudge cannot be read, or `null` when it can — same checked-before-describing shape as `invalidEventReason`. */
|
|
232
|
+
export function invalidNudgeReason(value) {
|
|
233
|
+
if (!isRecord(value))
|
|
234
|
+
return "the nudge is not an object";
|
|
235
|
+
if (typeof value.idleMinutes !== "number" || !Number.isFinite(value.idleMinutes)) {
|
|
236
|
+
return "idleMinutes is not a number";
|
|
237
|
+
}
|
|
238
|
+
if (!isNudgeCardRefArray(value.startable))
|
|
239
|
+
return "startable is not a {id,title}[] array";
|
|
240
|
+
if (!isNudgeCardRefArray(value.held))
|
|
241
|
+
return "held is not a {id,title}[] array";
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function cardList(cards) {
|
|
245
|
+
return cards.map((c) => `${c.id} "${c.title}"`).join(", ");
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The one readable line for a nudge (DX-2885). Always names the actual
|
|
249
|
+
* waiting cards — never a generic "keep going" — because that is the whole
|
|
250
|
+
* point of the feature: a session that only sees "you're idle" learns
|
|
251
|
+
* nothing a bare timer couldn't have told it. Always a single line; throws,
|
|
252
|
+
* naming the fault, on a malformed nudge.
|
|
253
|
+
*/
|
|
254
|
+
export function formatNudgeLine(event) {
|
|
255
|
+
const invalid = invalidNudgeReason(event);
|
|
256
|
+
if (invalid !== null)
|
|
257
|
+
throw new Error(`malformed nudge event: ${invalid}`);
|
|
258
|
+
const parts = [];
|
|
259
|
+
if (event.startable.length > 0)
|
|
260
|
+
parts.push(`startable: ${cardList(event.startable)}`);
|
|
261
|
+
if (event.held.length > 0)
|
|
262
|
+
parts.push(`held: ${cardList(event.held)}`);
|
|
263
|
+
return oneLine(`${LINE_PREFIX} this session has been idle ${event.idleMinutes}m with work waiting — ${parts.join("; ")}`);
|
|
264
|
+
}
|
|
180
265
|
/**
|
|
181
266
|
* Incremental SSE parser. Comment lines (keep-alives, the connect marker) and
|
|
182
267
|
* field-less blocks produce no message, which is what keeps them out of the output.
|
|
@@ -305,6 +390,36 @@ export async function runListener(options, deps) {
|
|
|
305
390
|
// elsewhere — see `ListenStopReason`'s docblock.
|
|
306
391
|
return { kind: "ended", reason: isKnownWireEndReason(reason) ? reason : "unknown" };
|
|
307
392
|
}
|
|
393
|
+
if (message.event === "nudge") {
|
|
394
|
+
// DX-2885 — a nudge carries no usable id (it is advisory, re-derived
|
|
395
|
+
// fresh every tick, never replayed — see `plan-session-stream.ts`'s
|
|
396
|
+
// module doc), so it rides the SAME `{type:"event", id:null, text}`
|
|
397
|
+
// shape an ordinary activity line uses (`id` is `null` only for a frame
|
|
398
|
+
// that carried no usable id — a nudge is exactly that case). That is
|
|
399
|
+
// what lets `bridge.ts` and the plugin's relay
|
|
400
|
+
// (`danxbot/scripts/plan-event-bridge.mjs`, outside this repo) forward
|
|
401
|
+
// it with NO changes at all: both already forward any `{type:"event",
|
|
402
|
+
// id, text}` line generically, keyed only on `type`.
|
|
403
|
+
//
|
|
404
|
+
// DX-2885 — this frame must stay exempt from DX-2730 D4's digest-
|
|
405
|
+
// batching when that lands (see DX-2730 AC 29939, and the matching note
|
|
406
|
+
// at the dashboard's own emit site): it must keep delivering
|
|
407
|
+
// immediately, never held for a later digest flush.
|
|
408
|
+
let parsed;
|
|
409
|
+
let invalid;
|
|
410
|
+
try {
|
|
411
|
+
parsed = JSON.parse(message.data);
|
|
412
|
+
invalid = invalidNudgeReason(parsed);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
invalid = `not JSON: ${err.message}`;
|
|
416
|
+
}
|
|
417
|
+
const text = invalid === null
|
|
418
|
+
? formatNudgeLine(parsed)
|
|
419
|
+
: `${LINE_PREFIX} could not read nudge (${invalid}): ${oneLine(message.data, 300)}`;
|
|
420
|
+
deps.write({ type: "event", id: null, text });
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
308
423
|
if (message.event !== "activity")
|
|
309
424
|
return null;
|
|
310
425
|
// `Number(null)` is 0, so a frame with no id must never be read as event 0: that id
|
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.86",
|
|
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",
|