dsh-batch-triage
Deterministic ticket triage: a rule prefilter classifies clear cases and returns everything else as needs_review instead of guessing.
Install
dsh plugin --profile web add dsh-batch-triage
Sandbox test
This plugin has not been tested yet. Submit a test →
README
dsh-batch-triage
**Rules decide the easy tickets. Anything the rules cannot place is handed to a
human — never quietly filed as routine.**
A DeepSeek Harness plugin.
No model, no service, no network, no API key.
$ node -e "
import('dsh-batch-triage').then(m => {
const r = m.triageBatch([
{ id: 'a', text: 'The service is down for all users' },
{ id: 'b', text: 'How do I install this on Windows?' },
{ id: 'c', text: 'The frobnitz is blorp.' },
], m.resolveConfig({}));
console.log(r.items.map(i => i.id + ' ' + i.class + ' ' + i.reasonCodes[0]).join('\n'));
console.log('exit', r.exitCode, r.exitBasis);
})"
a urgent OUTAGE_REPORTED
b routine INSTALL_QUESTION
c needs_review NO_RULE_MATCHED
exit 6 modelUnavailable
Quick start
dsh plugin --profile web add dsh-batch-triage@0.1.4
batch_triage is now available; Rules place the tickets they can; anything they cannot place is returned as needs_review — never quietly filed as routine.
Why rules first
A ticket batch is the wrong shape for one model call per item: the volume is high,
the interesting items are few, and every item costs a round trip whether or not it
needed one. So the order is inverted from the obvious one.
The prefilter runs on every item, first. Rules place the clear cases — a crash
with a stack trace is urgent, "how do I install this" is routine — and they place
them without asking anything. Only the residue would ever reach a model, and this
release does not reach one at all: it says so instead.
That ordering is not an optimisation. It is the design's main path, and it changes
what the tool is for: the prefilter is the product, and the model is a fallback.
The rule that matters most
An item is never routine unless a rule said so.
Every path that cannot classify — an empty body, no matching rule, no model
configured, a budget exhausted — produces needs_review. Turning "I could not
tell" into "nothing to see here" is the one failure this tool exists to prevent,
and the test suite pins it in both directions.
0.1.2 tightened what "a rule said so" is allowed to mean. A rule decides on a
phrase, and a ticket has more than one sentence: when the phrase that matched is a
question and another sentence reports something broken, the question rule does not get
to decide the ticket. It becomes needs_review with MALFUNCTION_REPORTED, and the
rule that would have swallowed it stays in reasonCodes so a reader sees both halves.
decidedBy says which tier actually decided:
value · meaning
prefilter · a rule placed it; reasonCodes names which
unavailable · nothing could place it and no model tier is configured
budget · a cap was reached, so it was not assessed
The last two both carry escalate: true. Neither is a verdict about the ticket.
What the rules look at
A structured field outranks the body text: severity: critical,
security: true, data_loss: true, reporter_tier: enterprise. A reporter who
filled in a form has already told you, and no wording in the body can lower what the
field said. Wording that reports something worse still raises it — a field can never
bury an outage, and when the two disagree the item carries both reason codes.
Otherwise the highest class any rule claims wins, and the table is written
strongest first so that a tie inside a class goes to the rule listed first:
class · rules
urgent · outage, data_loss, security, crash_with_trace, blocked_workflow
needs_review · regression, wrong_result, performance, flaky
routine · install_question, usage_question, documentation, feature_request, thanks
A ticket that reports an outage and phrases itself as a question must not be
swallowed by the question rule. That is a test — and in 0.1.1 it was a test that could
not fail, because its fixture used wording the outage rule already recognised, so the
fixture and the rule shared an assumption and the precedence was never really exercised.
Precedence is by class rather than by position now, which also means a caller-supplied
rules array cannot reorder the table into a fail-open.
Class precedence alone was not enough, and the six measured tickets below are why.
outage did not know "returns 500 for every request", so no severity rule matched,
and the question rule decided the whole ticket from one sentence of it. So a routine
verdict gets one more check: if the body reports a malfunction — an error, a crash,
something "unusable", a 5xx, deleted records, a leaked credential — in wording no
severity rule could name, the item becomes needs_review with MALFUNCTION_REPORTED
instead of routine. That guard only ever refuses a routine; it cannot raise
anything to urgent, and it cannot touch a verdict a severity rule already made.
Precedence by class is chosen over "most specific wins" deliberately. A scoring system
would need weights nobody could defend; a class ladder needs none, and the table stays
inspectable — a rule that never fires is a bug you can see.
The wordings the fix was measured against, verbatim from the audit. All six were
routine with exit 0 on 0.1.1:
ticket · 0.1.1 · 0.1.2
How do I install this? Also our production API returns 500 for every request. · routine INSTALL_QUESTION, exit 0 · urgent OUTAGE_REPORTED, exit 1
Is there documentation for the auth flow? We are seeing records deleted from our database. · routine DOCUMENTATION_REQUEST, exit 0 · urgent DATA_LOSS_REPORTED, exit 1
Feature request: please add CSV export. Meanwhile the app is unusable for our whole team. · routine FEATURE_REQUEST, exit 0 · urgent OUTAGE_REPORTED, exit 1
How do I configure SSRF protection? Our pentest found we can read other users files. · routine `INSTALL_QUESTION
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::6,最后更新 2026-10-20