ticketlens 0.38.47 → 0.38.49
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 +5 -1
- package/package.json +1 -1
- package/skills/jtb/SKILL.md +12 -3
- package/skills/jtb/scripts/fetch-ticket.mjs +7 -1
- package/skills/jtb/scripts/lib/api-utils.mjs +1 -1
- package/skills/jtb/scripts/lib/compliance-checker.mjs +1 -1
- package/skills/jtb/scripts/lib/consensus-checker.mjs +192 -0
- package/skills/jtb/scripts/lib/help.mjs +15 -1
- package/skills/jtb/scripts/lib/mcp-server.mjs +4 -1
- package/skills/jtb/scripts/lib/mcp-tool-schemas.mjs +2 -1
- package/skills/jtb/scripts/lib/summarizer.mjs +4 -1
package/README.md
CHANGED
|
@@ -294,10 +294,14 @@ Flag validation provides actionable hints:
|
|
|
294
294
|
ticketlens compliance <TICKET-KEY> # Check ticket requirements against local diff [Pro/Free 3/mo]
|
|
295
295
|
ticketlens compliance <TICKET-KEY> --profile=acme # Specify a profile
|
|
296
296
|
ticketlens compliance <TICKET-KEY> --plain # Plain markdown output
|
|
297
|
+
ticketlens compliance <TICKET-KEY> --consensus # Multi-agent AI review instead of the local matcher [Pro]
|
|
298
|
+
ticketlens compliance <TICKET-KEY> --consensus -y # Same, skipping the cost-confirmation prompt
|
|
297
299
|
```
|
|
298
300
|
|
|
299
301
|
Runs the same compliance check as `ticketlens CNV1-2 --compliance` but as a dedicated subcommand — useful when you want to check compliance without fetching the full ticket brief. Free accounts get 3 checks per month; Pro is unlimited.
|
|
300
302
|
|
|
303
|
+
`--consensus` (Pro) replaces the local deterministic matcher with independent reviews from your team's AI provider pool, reconciled by majority vote after a disagreement-triggered refinement round. Requires `ticketlens login` and a "consensus" role set up at Console > Admin > AI Roles, with 2+ providers attached from your team's shared registry (Console > Admin > AI Provider Pool — any title/key/endpoint/model, not a fixed list). This is the only compliance path that sends your diff off-machine — to TicketLens's backend, which fans out to each provider (keys are encrypted server-side and never sent to the CLI). The diff is scanned for secrets before anything is sent; a detected secret blocks the run with no AI call made. Prompts for confirmation before spending your team's API credits unless `-y`/`--yes` is passed.
|
|
304
|
+
|
|
301
305
|
---
|
|
302
306
|
|
|
303
307
|
### Compliance Ledger
|
|
@@ -456,7 +460,7 @@ Every note is scanned before saving — anything shaped like a real secret (API
|
|
|
456
460
|
|
|
457
461
|
**Removing a note:** `ticketlens note delete --id="..." [--ticket=KEY]` removes a note from your local vault. Local only — if it was already pushed to a team, teammates who pulled it keep their copy; deleting it there too is a manager action from the Console (Admin > Recall).
|
|
458
462
|
|
|
459
|
-
**Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `fetch`, `triage`, `compliance`, `review`, `standup`, `pr`, `stats`, `issue_types`, `history`, `collisions`, `ledger`, `doctor`, `recall_add`, `recall_update`, `recall_delete`, `recall_search`, `ticket_comment`, `ticket_transition`, `ticket_assign`, `ticket_duplicates`, `ticket_link`, `ticket_update`, and `ticket_create` as native tools — every CLI action now has an MCP tool, so any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same license gate per tool, same secret scan/local vault/tracker writes, same team sync — nothing is reimplemented. `fetch`, `doctor`, and `standup` are Free; `triage`'s base scan is Free with some options gated Pro/Team, same as the CLI (`ticketlens triage --help`); `compliance` and `pr` are Free, sharing a 3-checks/month cap on their requirements-coverage section, Pro unlimited; `review` is Free for branch/files/ticket context, with its coverage/focus section requiring Pro as a plain license check — it does not draw from that same monthly counter; `stats` is Free with a 7-day lookback cap, Pro extends it to 30 days, same split as the CLI (`ticketlens stats --help`); `issue_types` is Free and Jira-only — pre-fetches and caches a profile's real creatable projects and issue types ahead of a `ticket_create` attempt, sharing its cache with that tool's own reactive enrichment; Linear/GitHub profiles get a clear "not available" instead of an empty result; `history` reads local triage history only (zero network) and requires Pro; `collisions` requires `ticketlens login` (Console access) plus a Team license; `ledger` exports the local, signed compliance audit trail (zero network) and requires Pro; every other tool needs Pro. `recall_update` overwrites an existing Recall note's body — internal plumbing for the note quality loop, not typically called directly; its `attachments` array appends new files to whatever the note already has, same as `recall_add`'s, never replacing existing ones. `recall_delete` is destructive and local-vault-only — requires `confirm: true` alongside `id` to actually execute; there is no interactive y/N prompt under MCP (no real terminal to prompt against), so omitting it always fails rather than silently blocking. Point your harness's MCP config at it: `{ "command": "ticketlens", "args": ["mcp"] }` — or run `ticketlens mcp install` in a project to write that entry into its `.mcp.json` for you (creates the file if it doesn't exist, merges in if it does — never touches any other entry already there; `--dry-run` to preview first).
|
|
463
|
+
**Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `fetch`, `triage`, `compliance`, `review`, `standup`, `pr`, `stats`, `issue_types`, `history`, `collisions`, `ledger`, `doctor`, `recall_add`, `recall_update`, `recall_delete`, `recall_search`, `ticket_comment`, `ticket_transition`, `ticket_assign`, `ticket_duplicates`, `ticket_link`, `ticket_update`, and `ticket_create` as native tools — every CLI action now has an MCP tool, so any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same license gate per tool, same secret scan/local vault/tracker writes, same team sync — nothing is reimplemented. `fetch`, `doctor`, and `standup` are Free; `triage`'s base scan is Free with some options gated Pro/Team, same as the CLI (`ticketlens triage --help`); `compliance` and `pr` are Free, sharing a 3-checks/month cap on their requirements-coverage section, Pro unlimited; `compliance` additionally accepts `consensus: true` (Pro) to replace the local deterministic matcher with a multi-agent AI review, run server-side against a "consensus" role configured at Console > Admin > AI Roles (2+ providers from the team's shared pool), and always implies `--yes` under MCP since there's no TTY for the cost-confirmation prompt; `review` is Free for branch/files/ticket context, with its coverage/focus section requiring Pro as a plain license check — it does not draw from that same monthly counter; `stats` is Free with a 7-day lookback cap, Pro extends it to 30 days, same split as the CLI (`ticketlens stats --help`); `issue_types` is Free and Jira-only — pre-fetches and caches a profile's real creatable projects and issue types ahead of a `ticket_create` attempt, sharing its cache with that tool's own reactive enrichment; Linear/GitHub profiles get a clear "not available" instead of an empty result; `history` reads local triage history only (zero network) and requires Pro; `collisions` requires `ticketlens login` (Console access) plus a Team license; `ledger` exports the local, signed compliance audit trail (zero network) and requires Pro; every other tool needs Pro. `recall_update` overwrites an existing Recall note's body — internal plumbing for the note quality loop, not typically called directly; its `attachments` array appends new files to whatever the note already has, same as `recall_add`'s, never replacing existing ones. `recall_delete` is destructive and local-vault-only — requires `confirm: true` alongside `id` to actually execute; there is no interactive y/N prompt under MCP (no real terminal to prompt against), so omitting it always fails rather than silently blocking. Point your harness's MCP config at it: `{ "command": "ticketlens", "args": ["mcp"] }` — or run `ticketlens mcp install` in a project to write that entry into its `.mcp.json` for you (creates the file if it doesn't exist, merges in if it does — never touches any other entry already there; `--dry-run` to preview first).
|
|
460
464
|
|
|
461
465
|
`note add`'s save confirmation and `recall`'s search results are styled by default in a terminal; add `--plain` to either for bare, pipe-safe output. `recall` always shows each note's file ID (e.g. `[1784135399545-fe01c4.md]`) so you can open it directly (`cat ~/.ticketlens/recall/<PREFIX>/<id>`), or pass `--full` to print the full body content inline instead. Each result shows a relative time (`2h ago`, `3d ago`) rather than a bare date — the full-precision timestamp is always in the note file's own frontmatter.
|
|
462
466
|
|
package/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.
|
|
1
|
+
<!-- jtb-skill-version: 0.43.0 -->
|
|
2
2
|
---
|
|
3
3
|
name: jtb
|
|
4
4
|
description: Fetch a Jira ticket's full context (description, comments, linked issues, code references) and assemble a structured TicketBrief for implementation planning. Use when user types /jtb, mentions a Jira ticket key, or wants to plan work from a Jira ticket.
|
|
@@ -529,12 +529,21 @@ Use this evaluation order:
|
|
|
529
529
|
3. **Manual checklist** — list each requirement for the developer to verify manually
|
|
530
530
|
|
|
531
531
|
### Privacy
|
|
532
|
-
`--compliance` never sends data anywhere. The diff stays local. All analysis is performed by Claude Code within your session context.
|
|
532
|
+
`--compliance` never sends data anywhere. The diff stays local. All analysis is performed by Claude Code within your session context. (The standalone command's `--consensus` opt-in below is the one exception — see that section.)
|
|
533
533
|
|
|
534
534
|
### Standalone command
|
|
535
535
|
The same tier-gated check also runs as its own command — `ticketlens compliance PROJ-123` — independent of a full ticket fetch. This is what `ticketlens install-hooks` wires into a pre-push git hook (`ticketlens compliance "$KEY" || exit 1`, gated on a configurable coverage threshold). It shares the same `FREE_LIMIT`/Pro gate and the same compliance ledger as the `--compliance` flag above.
|
|
536
536
|
|
|
537
|
-
If this harness has TicketLens's MCP server configured (a tool named `compliance` — often shown as `mcp__ticketlens__compliance` — visible in your tool list), prefer it over the bash form: same tier gate (Free: 3 checks/month, Pro: unlimited), same report — just no shell command to construct or stdout to parse. It accepts `ticket`/`profile`, matching the standalone command's arguments
|
|
537
|
+
If this harness has TicketLens's MCP server configured (a tool named `compliance` — often shown as `mcp__ticketlens__compliance` — visible in your tool list), prefer it over the bash form: same tier gate (Free: 3 checks/month, Pro: unlimited), same report — just no shell command to construct or stdout to parse. It accepts `ticket`/`profile`/`consensus`, matching the standalone command's arguments below.
|
|
538
|
+
|
|
539
|
+
### --consensus: Multi-Agent AI Review (standalone command only, Pro)
|
|
540
|
+
`ticketlens compliance PROJ-123 --consensus` replaces the local deterministic keyword-diff matcher with a multi-agent AI review, for a higher-confidence check on requirements that plain keyword matching tends to misjudge (paraphrased requirements, negative conditions, cross-file logic).
|
|
541
|
+
|
|
542
|
+
- **Requires `ticketlens login`** and a **consensus role** configured at Console > Admin > AI Roles, with 2+ providers attached from your team's shared registry (Console > Admin > AI Provider Pool — any title/key/endpoint/model your team wants, not a fixed vendor list; managed by team managers, selected per-role by each teammate). Provider keys are encrypted server-side and never sent to the CLI, so the actual AI calls run on TicketLens's backend, not from your machine directly — **this is the one path in TicketLens where the diff leaves your machine, to TicketLens's own backend, which fans out to each provider.** Before anything is sent, the diff runs through the same secret scanner Recall notes use — a detected secret blocks the run entirely, before any AI call.
|
|
543
|
+
- Each attached provider independently reviews every requirement; where agents disagree, a second refinement round shows each agent its peers' (anonymized) verdicts and lets it reconsider; final verdicts are reconciled by majority vote, ties broken toward the stricter verdict. The report always shows the full per-agent breakdown, including any round-1→round-2 change.
|
|
544
|
+
- Prompts for confirmation before making any API calls (real cost against your team's provider keys) unless `-y`/`--yes` is passed. Over MCP, `consensus: true` always implies yes (no TTY to prompt).
|
|
545
|
+
- The now-superseded first version (0.38.48, same-day) read `~/.ticketlens/credentials.json` and called Anthropic/OpenAI/Groq directly from the CLI — deprecated before any stable release once the dynamic, Console-managed provider registry replaced it.
|
|
546
|
+
- `Phase 2` (not yet built): non-`anthropic`/`openai_compatible` provider shapes beyond the two `provider_type`s the registry supports today. `Phase 1.5` (not yet built): scored iterative refinement with a stronger-model arbiter judging consensus acceptability, instead of plain majority vote.
|
|
538
547
|
|
|
539
548
|
### Ledger export
|
|
540
549
|
`ticketlens ledger [--format=json|csv]` exports the same local, signed compliance ledger these checks write to — entirely local, no network call. `[Pro]`. If this harness has TicketLens's MCP server configured (a tool named `ledger` — often shown as `mcp__ticketlens__ledger` — visible in your tool list), prefer it over the bash form: same tier gate, same export, just no shell command to construct or stdout to parse. It accepts `format` (`json`/`csv`, defaults to `json`).
|
|
@@ -33,6 +33,7 @@ import { apiBase } from './lib/api-utils.mjs';
|
|
|
33
33
|
import { isLicensed, showUpgradePrompt, readLicense } from './lib/license.mjs';
|
|
34
34
|
import { detectVcs } from './lib/vcs-detector.mjs';
|
|
35
35
|
import { runComplianceCheck } from './lib/compliance-checker.mjs';
|
|
36
|
+
import { runConsensusCheck } from './lib/consensus-checker.mjs';
|
|
36
37
|
import { fetchRemoteLinks, buildAuthHeader } from './lib/jira-client.mjs';
|
|
37
38
|
import { fetchConfluencePage } from './lib/confluence-client.mjs';
|
|
38
39
|
|
|
@@ -683,13 +684,18 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
683
684
|
const codeRefsC = extractCodeReferences(allTextC);
|
|
684
685
|
const briefC = assembleBrief(ticketC, codeRefsC);
|
|
685
686
|
|
|
686
|
-
const
|
|
687
|
+
const useConsensus = args.includes('--consensus');
|
|
688
|
+
const forceYes = args.includes('--yes') || args.includes('-y');
|
|
689
|
+
const complianceRunner = useConsensus
|
|
690
|
+
? (opts.runConsensusCheck ?? runConsensusCheck)
|
|
691
|
+
: (opts.runComplianceCheck ?? runComplianceCheck);
|
|
687
692
|
const complianceResult = await complianceRunner({
|
|
688
693
|
brief: briefC,
|
|
689
694
|
description: ticketC.description,
|
|
690
695
|
ticketKey: ticketKeyArg,
|
|
691
696
|
configDir: resolvedConfigDir,
|
|
692
697
|
stream: errStream,
|
|
698
|
+
...(useConsensus ? { forceYes } : {}),
|
|
693
699
|
});
|
|
694
700
|
|
|
695
701
|
if (complianceResult === null) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Centralised here to avoid triplicating the regex and warning logic.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_API_BASE = '
|
|
6
|
+
export const DEFAULT_API_BASE = 'https://api.ticketlens.app';
|
|
7
7
|
export const DEFAULT_SITE_BASE = 'https://ticketlens.app';
|
|
8
8
|
|
|
9
9
|
// Matches localhost, 127.0.0.1, and any hostname ending in .test or .local,
|
|
@@ -19,7 +19,7 @@ export function statusColor(status, s) {
|
|
|
19
19
|
// Shared with matchColor in ticket-command.mjs (duplicates' match-confidence
|
|
20
20
|
// tiers) — same 70/50 thresholds, same green/yellow/dim vocabulary, applied
|
|
21
21
|
// here to overall requirement coverage instead of a single match score.
|
|
22
|
-
function coverageColor(pct, s) {
|
|
22
|
+
export function coverageColor(pct, s) {
|
|
23
23
|
if (pct >= 70) return s.green;
|
|
24
24
|
if (pct >= 50) return s.yellow;
|
|
25
25
|
return s.dim;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ticketlens compliance TICKET --consensus — thin client over POST /v1/consensus.
|
|
3
|
+
*
|
|
4
|
+
* Superseded local-BYOK version (0.38.48): read ~/.ticketlens/credentials.json,
|
|
5
|
+
* called Anthropic/OpenAI/Groq directly from the CLI. Deprecated same session it
|
|
6
|
+
* shipped — provider keys now live in the group-shared, dynamic AiProviderPool
|
|
7
|
+
* registry (Console > Admin > AI Provider Pool / AI Roles), encrypted server-side
|
|
8
|
+
* and never sent to the CLI, so the actual AI calls must happen on the backend.
|
|
9
|
+
* The diff and requirements still only ever leave the machine to reach
|
|
10
|
+
* TicketLens's own backend now (not straight to each vendor) — see
|
|
11
|
+
* ConsensusController.php for the round-1/round-2/reconcile algorithm, a direct
|
|
12
|
+
* port of what used to run here.
|
|
13
|
+
*/
|
|
14
|
+
import { isLicensed, showUpgradePrompt } from './license.mjs';
|
|
15
|
+
import { extractRequirements } from './requirement-extractor.mjs';
|
|
16
|
+
import { findLinkedCommits } from './commit-linker.mjs';
|
|
17
|
+
import { readCliToken } from './cli-auth.mjs';
|
|
18
|
+
import { apiBase } from './api-utils.mjs';
|
|
19
|
+
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
20
|
+
import { createStyler } from './ansi.mjs';
|
|
21
|
+
import { STATUS_ICON, statusColor, coverageColor } from './compliance-checker.mjs';
|
|
22
|
+
|
|
23
|
+
const ROLES_PATH = '/v1/ai-provider-roles';
|
|
24
|
+
const CONSENSUS_PATH = '/v1/consensus';
|
|
25
|
+
const ROLES_TIMEOUT_MS = 10_000;
|
|
26
|
+
const CONSENSUS_TIMEOUT_MS = 90_000; // two AI rounds across N providers — real work, not a quick API call
|
|
27
|
+
|
|
28
|
+
/** Interactive y/N cost-confirmation gate — same non-interactive fallback shape as confirmDestructive. */
|
|
29
|
+
async function confirmCost(providerCount, { stream = process.stderr, stdin = process.stdin } = {}) {
|
|
30
|
+
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
31
|
+
stream.write(' Non-interactive mode: pass --yes/-y to run --consensus without a prompt.\n');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
stream.write(` --consensus will run ${providerCount} AI review(s) via your team's provider pool. Continue? y/N `);
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
stdin.setRawMode(true);
|
|
37
|
+
stdin.resume();
|
|
38
|
+
stdin.once('data', buf => {
|
|
39
|
+
stdin.setRawMode(false);
|
|
40
|
+
stdin.pause();
|
|
41
|
+
const confirmed = buf.toString().toLowerCase() === 'y';
|
|
42
|
+
stream.write(confirmed ? 'y\n' : 'N\n');
|
|
43
|
+
resolve(confirmed);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatNoCriteriaReport(ticketKey, s) {
|
|
49
|
+
return [
|
|
50
|
+
'',
|
|
51
|
+
` Consensus Compliance Check — ${s.brand(s.bold(ticketKey))}`,
|
|
52
|
+
` ${s.dim('─'.repeat(50))}`,
|
|
53
|
+
'',
|
|
54
|
+
' No acceptance criteria found in ticket description.',
|
|
55
|
+
' Add a "Acceptance Criteria" section or Given/When/Then statements.',
|
|
56
|
+
'',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatConsensusReport({ ticketKey, results, perAgent, disagreedCount, s }) {
|
|
61
|
+
const lines = [
|
|
62
|
+
'',
|
|
63
|
+
` Consensus Compliance Check — ${s.brand(s.bold(ticketKey))}`,
|
|
64
|
+
` ${s.dim('─'.repeat(50))}`,
|
|
65
|
+
` ${s.dim(`${perAgent.length} agents: ${perAgent.map(a => a.title).join(', ')}`)}`,
|
|
66
|
+
'',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const { requirement, status } of results) {
|
|
70
|
+
const icon = statusColor(status, s)(STATUS_ICON[status] ?? '?');
|
|
71
|
+
lines.push(` ${icon} ${requirement}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lines.push('');
|
|
75
|
+
const found = results.filter(r => r.status === 'FOUND').length;
|
|
76
|
+
const coveragePercent = Math.round(((found + results.filter(r => r.status === 'PARTIAL').length * 0.5) / results.length) * 100);
|
|
77
|
+
lines.push(` Coverage: ${coverageColor(coveragePercent, s)(`${coveragePercent}%`)} (${found}/${results.length} requirements found)`);
|
|
78
|
+
if (disagreedCount > 0) {
|
|
79
|
+
lines.push(` ${s.dim(`${disagreedCount} requirement(s) needed a refinement round (agents initially disagreed).`)}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(` ${s.bold('Per-agent breakdown:')}`);
|
|
83
|
+
for (const { title, verdicts, round1Verdicts } of perAgent) {
|
|
84
|
+
const parts = verdicts.map((v, i) => (round1Verdicts[i] !== v ? `${round1Verdicts[i]}→${v}` : v));
|
|
85
|
+
lines.push(` ${s.dim(title)}: ${parts.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
|
|
89
|
+
return { report: lines.join('\n'), coveragePercent };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function fetchJson(url, { fetcher, timeoutMs, ...init }) {
|
|
93
|
+
const res = await fetcher(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
94
|
+
let body = null;
|
|
95
|
+
try { body = await res.json(); } catch { /* non-JSON error page — body stays null */ }
|
|
96
|
+
return { ok: res.ok, status: res.status, body };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runConsensusCheck({
|
|
100
|
+
brief,
|
|
101
|
+
description = null,
|
|
102
|
+
ticketKey,
|
|
103
|
+
configDir = DEFAULT_CONFIG_DIR,
|
|
104
|
+
stream = process.stderr,
|
|
105
|
+
outStream = process.stdout,
|
|
106
|
+
forceYes = false,
|
|
107
|
+
stdin = process.stdin,
|
|
108
|
+
cliToken,
|
|
109
|
+
isLicensedFn = isLicensed,
|
|
110
|
+
showUpgradeFn = showUpgradePrompt,
|
|
111
|
+
extractRequirementsFn = extractRequirements,
|
|
112
|
+
findLinkedCommitsFn = findLinkedCommits,
|
|
113
|
+
confirmCostFn = confirmCost,
|
|
114
|
+
readCliTokenFn = readCliToken,
|
|
115
|
+
fetcher = globalThis.fetch,
|
|
116
|
+
}) {
|
|
117
|
+
if (!isLicensedFn('pro', configDir)) {
|
|
118
|
+
showUpgradeFn('pro', '--consensus', { stream });
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const requirements = extractRequirementsFn(description ?? brief);
|
|
123
|
+
const s = createStyler({ isTTY: outStream.isTTY });
|
|
124
|
+
|
|
125
|
+
if (requirements.length === 0) {
|
|
126
|
+
return { report: formatNoCriteriaReport(ticketKey, s), results: [], coveragePercent: 0, noCriteria: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const token = cliToken ?? readCliTokenFn(configDir);
|
|
130
|
+
if (!token) {
|
|
131
|
+
stream.write(' ✖ --consensus requires a login. Run: ticketlens login\n');
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pre-flight: know the provider count before prompting for cost, and give a
|
|
136
|
+
// specific, actionable error before ever touching /v1/consensus — the
|
|
137
|
+
// backend re-validates all of this too, this is purely a faster/clearer UX path.
|
|
138
|
+
const rolesRes = await fetchJson(`${apiBase()}${ROLES_PATH}`, {
|
|
139
|
+
fetcher, timeoutMs: ROLES_TIMEOUT_MS,
|
|
140
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
141
|
+
}).catch(err => ({ ok: false, status: 0, body: null, networkError: err }));
|
|
142
|
+
|
|
143
|
+
if (!rolesRes.ok) {
|
|
144
|
+
stream.write(' ✖ Could not reach TicketLens to check your consensus role. Try again, or check your connection.\n');
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const consensusRole = (rolesRes.body?.roles ?? []).find(r => r.kind === 'consensus');
|
|
149
|
+
if (!consensusRole) {
|
|
150
|
+
stream.write(' ✖ No consensus role configured. Set one up in Console > Admin > AI Providers.\n');
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (consensusRole.providers.length < 2) {
|
|
154
|
+
stream.write(` ✖ Your consensus role needs at least 2 providers — currently has ${consensusRole.providers.length}. Add more in Console > Admin > AI Roles.\n`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!forceYes) {
|
|
159
|
+
const proceed = await confirmCostFn(consensusRole.providers.length, { stream, stdin });
|
|
160
|
+
if (!proceed) {
|
|
161
|
+
stream.write(' Aborted — no request made.\n');
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { diff } = findLinkedCommitsFn(ticketKey, { cwd: process.cwd() });
|
|
167
|
+
|
|
168
|
+
const runRes = await fetchJson(`${apiBase()}${CONSENSUS_PATH}`, {
|
|
169
|
+
fetcher, timeoutMs: CONSENSUS_TIMEOUT_MS,
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
172
|
+
body: JSON.stringify({ ticketKey, diff, requirements }),
|
|
173
|
+
}).catch(err => ({ ok: false, status: 0, body: null, networkError: err }));
|
|
174
|
+
|
|
175
|
+
if (!runRes.ok) {
|
|
176
|
+
const msg = runRes.body?.error
|
|
177
|
+
?? (runRes.networkError?.name === 'TimeoutError' ? 'Request timed out.' : runRes.networkError?.message)
|
|
178
|
+
?? `HTTP ${runRes.status}`;
|
|
179
|
+
stream.write(` ✖ ${msg}\n`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const warning of runRes.body.warnings ?? []) {
|
|
184
|
+
stream.write(` Warning: ${warning}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const { report, coveragePercent } = formatConsensusReport({
|
|
188
|
+
ticketKey, results: runRes.body.results, perAgent: runRes.body.perAgent, disagreedCount: runRes.body.disagreedCount, s,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return { report, results: runRes.body.results, coveragePercent, noCriteria: false };
|
|
192
|
+
}
|
|
@@ -658,6 +658,9 @@ export function printMcpHelp({ stream = process.stdout } = {}) {
|
|
|
658
658
|
` ${s.cyan('fetch')}, ${s.cyan('doctor')}, and ${s.cyan('standup')} are Free; ${s.cyan('triage')} is Free with some Pro/Team-gated`,
|
|
659
659
|
` options (see ${s.cyan('ticketlens triage --help')}); ${s.cyan('compliance')} and ${s.cyan('pr')} are Free, sharing a`,
|
|
660
660
|
` 3-checks/month cap on their requirements-coverage section, Pro unlimited;`,
|
|
661
|
+
` ${s.cyan('compliance')}'s \`consensus: true\` (Pro) runs a multi-agent review server-side against`,
|
|
662
|
+
` a "consensus" role configured at Console > Admin > AI Roles, and always implies`,
|
|
663
|
+
` \`--yes\` under MCP, since there is no TTY for the cost-confirmation prompt;`,
|
|
661
664
|
` ${s.cyan('review')} is Free for branch/files/ticket context — its coverage/focus section`,
|
|
662
665
|
` requires Pro as a plain license check, not a draw on that same counter;`,
|
|
663
666
|
` ${s.cyan('stats')} is Free with a 7-day lookback, Pro extends it to 30 days, same split`,
|
|
@@ -1089,7 +1092,7 @@ export function printComplianceHelp({ stream = process.stdout } = {}) {
|
|
|
1089
1092
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
1090
1093
|
const lines = [
|
|
1091
1094
|
'',
|
|
1092
|
-
` ${s.bold(s.brand('ticketlens'))} ${s.bold('compliance')} ${s.dim('<TICKET-KEY> [--profile=NAME]')} ${s.dim('[Pro/Free 3/mo]')}`,
|
|
1095
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('compliance')} ${s.dim('<TICKET-KEY> [--profile=NAME] [--consensus [-y]]')} ${s.dim('[Pro/Free 3/mo]')}`,
|
|
1093
1096
|
'',
|
|
1094
1097
|
` Check your current branch's diff against the ticket's requirements.`,
|
|
1095
1098
|
` Extracts candidate requirements from the ticket description and diffs`,
|
|
@@ -1102,12 +1105,23 @@ export function printComplianceHelp({ stream = process.stdout } = {}) {
|
|
|
1102
1105
|
` ${s.bold('OPTIONS')}`,
|
|
1103
1106
|
'',
|
|
1104
1107
|
` ${s.brand('--profile')}=${s.dim('NAME')} Use a specific Jira profile`,
|
|
1108
|
+
` ${s.brand('--consensus')} Replace the local matcher with a multi-agent AI review ${s.dim('[Pro]')}`,
|
|
1109
|
+
` ${s.dim(' ')} — requires ${s.cyan('ticketlens login')} and a "consensus" role configured`,
|
|
1110
|
+
` ${s.dim(' ')} at Console > Admin > AI Roles (2+ providers from your team's`,
|
|
1111
|
+
` ${s.dim(' ')} shared pool, Console > Admin > AI Provider Pool). Runs server-side`,
|
|
1112
|
+
` ${s.dim(' ')} — the diff and requirements are sent to TicketLens's backend,`,
|
|
1113
|
+
` ${s.dim(' ')} scanned for secrets first, then to each provider. Reconciles`,
|
|
1114
|
+
` ${s.dim(' ')} disagreements with a refinement round, then a majority vote.`,
|
|
1115
|
+
` ${s.dim(' ')} Prompts for confirmation before spending your team's API credits.`,
|
|
1116
|
+
` ${s.brand('-y')}, ${s.brand('--yes')} Skip the --consensus cost confirmation prompt`,
|
|
1105
1117
|
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
1106
1118
|
'',
|
|
1107
1119
|
` ${s.bold('EXAMPLES')}`,
|
|
1108
1120
|
'',
|
|
1109
1121
|
` ${s.dim('$')} ticketlens compliance PROJ-123`,
|
|
1110
1122
|
` ${s.dim('$')} ticketlens compliance PROJ-123 --profile=myteam`,
|
|
1123
|
+
` ${s.dim('$')} ticketlens compliance PROJ-123 --consensus`,
|
|
1124
|
+
` ${s.dim('$')} ticketlens compliance PROJ-123 --consensus -y`,
|
|
1111
1125
|
'',
|
|
1112
1126
|
];
|
|
1113
1127
|
stream.write(lines.join('\n') + '\n');
|
|
@@ -174,9 +174,12 @@ async function callTriage(args, { configDir, runTriageFn }) {
|
|
|
174
174
|
* printErr through it before this tool existed — see the fetch tool's own
|
|
175
175
|
* shipping notes for why that treatment was deferred per-tool).
|
|
176
176
|
*/
|
|
177
|
-
function buildComplianceArgs({ ticket, profile }) {
|
|
177
|
+
function buildComplianceArgs({ ticket, profile, consensus }) {
|
|
178
178
|
const args = ['compliance', ticket];
|
|
179
179
|
if (profile) args.push(`--profile=${profile}`);
|
|
180
|
+
// MCP has no TTY to answer the interactive cost-confirmation prompt, so --consensus
|
|
181
|
+
// always implies --yes here — the caller already opted in by setting consensus: true.
|
|
182
|
+
if (consensus) args.push('--consensus', '--yes');
|
|
180
183
|
return args;
|
|
181
184
|
}
|
|
182
185
|
|
|
@@ -42,12 +42,13 @@ export const TOOLS = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'compliance',
|
|
45
|
-
description: 'Check a ticket\'s acceptance-criteria coverage against the current git diff — extracts requirements from the ticket description, matches them against code changes, and reports a coverage percentage plus what\'s missing. Read-only; the same check `ticketlens install-hooks` runs automatically. Free tier: 3 checks per month; TicketLens Pro removes the limit.',
|
|
45
|
+
description: 'Check a ticket\'s acceptance-criteria coverage against the current git diff — extracts requirements from the ticket description, matches them against code changes, and reports a coverage percentage plus what\'s missing. Read-only; the same check `ticketlens install-hooks` runs automatically. Free tier: 3 checks per month; TicketLens Pro removes the limit. Set `consensus: true` (TicketLens Pro) to replace the local matcher with a multi-agent AI review, run server-side — requires `ticketlens login` and a "consensus" role configured at Console > Admin > AI Roles (2+ providers attached from the team\'s shared pool, Console > Admin > AI Provider Pool). The diff is scanned for secrets before it is sent. Automatically skips the interactive cost-confirmation prompt since MCP has no TTY.',
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: 'object',
|
|
48
48
|
properties: {
|
|
49
49
|
ticket: { type: 'string', description: 'Ticket key, e.g. PROJ-123.' },
|
|
50
50
|
profile: { type: 'string', description: 'Connection profile to target, overriding folder-based inference and the default profile.' },
|
|
51
|
+
consensus: { type: 'boolean', description: 'Use multi-agent AI consensus instead of the local deterministic matcher. Requires TicketLens Pro and 2+ configured AI provider keys.' },
|
|
51
52
|
},
|
|
52
53
|
required: ['ticket'],
|
|
53
54
|
},
|
|
@@ -116,7 +116,10 @@ async function callGroq({ brief, apiKey, fetcher, timeoutMs, prompt, maxTokens }
|
|
|
116
116
|
'Authorization': `Bearer ${apiKey}`,
|
|
117
117
|
},
|
|
118
118
|
body: JSON.stringify({
|
|
119
|
-
|
|
119
|
+
// llama-3.1-8b-instant was decommissioned by Groq 2026-08-16 (HTTP 404 on
|
|
120
|
+
// every call since) — openai/gpt-oss-20b is Groq's own recommended
|
|
121
|
+
// replacement. See console.groq.com/docs/deprecations.
|
|
122
|
+
model: 'openai/gpt-oss-20b',
|
|
120
123
|
max_tokens: maxTokens,
|
|
121
124
|
messages: [{ role: 'user', content: prompt + brief }],
|
|
122
125
|
}),
|