ticketlens 0.38.33 → 0.38.34
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/package.json
CHANGED
|
@@ -18,6 +18,19 @@ export const NOTE_ADD_RE = /\bticketlens\s+note\s+add\b|\/jtb\s+note\b/;
|
|
|
18
18
|
// (mcp__<server-alias>__<tool-name>) — the server alias is whatever the user
|
|
19
19
|
// named it in their own .mcp.json, so only the tool-name suffix is fixed.
|
|
20
20
|
export const NOTE_ADD_MCP_RE = /^mcp__.+__recall_add$/;
|
|
21
|
+
// Matches jtb's fetch: the CLI's bare/default ticket-key form ("ticketlens
|
|
22
|
+
// PROJ-123" or its "tl" bin alias), the "get" alias form, and the /jtb skill
|
|
23
|
+
// wrapper. Deliberately excludes an explicit "fetch" subcommand — there is
|
|
24
|
+
// no such subcommand; parseCommand() (cli.mjs) only special-cases "get" as
|
|
25
|
+
// an alias, so "ticketlens fetch PROJ-123" falls through to the catch-all
|
|
26
|
+
// with "fetch" itself still in argv and errors as an invalid ticket key
|
|
27
|
+
// (live-verified during code review) — matching it here would silently
|
|
28
|
+
// swallow a broken invocation as if a real fetch had happened. Also
|
|
29
|
+
// deliberately does not match any other tracked subcommand (triage/
|
|
30
|
+
// compliance/etc) — those are lowercase words and can never satisfy the
|
|
31
|
+
// uppercase ticket-key class required immediately after the command name.
|
|
32
|
+
export const FETCH_RE = /\bticketlens\s+(?:get\s+)?[A-Z][A-Z0-9]{1,9}-\d+\b|\btl\s+(?:get\s+)?[A-Z][A-Z0-9]{1,9}-\d+\b|\/jtb\s+(?:get\s+)?[A-Z][A-Z0-9]{1,9}-\d+\b/;
|
|
33
|
+
export const FETCH_MCP_RE = /^mcp__.+__fetch$/;
|
|
21
34
|
|
|
22
35
|
export function readStdinJson() {
|
|
23
36
|
const raw = fs.readFileSync(0, 'utf8');
|
|
@@ -170,9 +183,19 @@ export function hasRecentNag(cwd, now = Date.now()) {
|
|
|
170
183
|
* early, and picking one deterministic match keeps this function decoupled
|
|
171
184
|
* from profiles.json (it stays a pure transcript reader; profile lookup
|
|
172
185
|
* and its own fallback chain belong entirely to resolveProfile()).
|
|
186
|
+
*
|
|
187
|
+
* `sawFetch` (backlog #15) is deliberately narrower than `sawTicketKey`: it
|
|
188
|
+
* only goes true when jtb's fetch tool actually ran (CLI or MCP form, same
|
|
189
|
+
* dual-detection shape as sawNoteAdd below) — not merely when a ticket-key-
|
|
190
|
+
* shaped string appears anywhere in the transcript. `sawTicketKey` false-
|
|
191
|
+
* positived on any incidental match (a doc, a code comment, a test fixture
|
|
192
|
+
* name like BETA-42), which is fine for its own low-stakes use (picking
|
|
193
|
+
* which profile's settings apply) but was wrong as shouldNag()'s nag
|
|
194
|
+
* precondition — SKILL.md's own capture guidance is scoped to "whenever
|
|
195
|
+
* jtb's fetch was used," so the hook now checks the same thing it backstops.
|
|
173
196
|
*/
|
|
174
197
|
export function scanTranscript(transcriptPath) {
|
|
175
|
-
const result = { sawTicketKey: false, sawRecallFlag: false, sawNoteAdd: false, ticketKey: null };
|
|
198
|
+
const result = { sawTicketKey: false, sawRecallFlag: false, sawNoteAdd: false, sawFetch: false, ticketKey: null };
|
|
176
199
|
let lines;
|
|
177
200
|
try {
|
|
178
201
|
lines = fs.readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
|
|
@@ -217,6 +240,10 @@ export function scanTranscript(transcriptPath) {
|
|
|
217
240
|
const isCliNoteAdd = block.name === 'Bash' && NOTE_ADD_RE.test(block.input?.command ?? '');
|
|
218
241
|
const isMcpNoteAdd = NOTE_ADD_MCP_RE.test(block.name ?? '');
|
|
219
242
|
if (isCliNoteAdd || isMcpNoteAdd) result.sawNoteAdd = true;
|
|
243
|
+
|
|
244
|
+
const isCliFetch = block.name === 'Bash' && FETCH_RE.test(block.input?.command ?? '');
|
|
245
|
+
const isMcpFetch = FETCH_MCP_RE.test(block.name ?? '');
|
|
246
|
+
if (isCliFetch || isMcpFetch) result.sawFetch = true;
|
|
220
247
|
}
|
|
221
248
|
}
|
|
222
249
|
}
|
|
@@ -235,9 +262,17 @@ export function scanTranscript(transcriptPath) {
|
|
|
235
262
|
* compaction/session_id rollover, or nagging more than once per session).
|
|
236
263
|
* Strict's actual effect on capture volume comes from SKILL.md's lowered
|
|
237
264
|
* in-session capture bar, not from this function.
|
|
265
|
+
*
|
|
266
|
+
* Gated on `sawFetch`, not `sawTicketKey` (backlog #15) — including the
|
|
267
|
+
* `sawRecallFlag` broken-promise case, which is why the gate is a blanket
|
|
268
|
+
* `!sawFetch` check rather than per-branch: a flag can't legitimately fire
|
|
269
|
+
* outside real ticket work, and this keeps the whole function's trigger
|
|
270
|
+
* matching SKILL.md's own capture-guidance scope exactly ("unconditionally
|
|
271
|
+
* whenever jtb's fetch was used"), instead of firing on any incidental
|
|
272
|
+
* ticket-key-shaped string.
|
|
238
273
|
*/
|
|
239
|
-
export function shouldNag({
|
|
240
|
-
if (!
|
|
274
|
+
export function shouldNag({ sawFetch, sawRecallFlag, sawNoteAdd, recallStrictness = 'balanced' }) {
|
|
275
|
+
if (!sawFetch || sawNoteAdd) return false;
|
|
241
276
|
if (recallStrictness === 'loose') return sawRecallFlag; // only the broken-promise case
|
|
242
|
-
return true; // balanced and strict:
|
|
277
|
+
return true; // balanced and strict: a fetch with no note is enough
|
|
243
278
|
}
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
* Stop hook — end-of-session Recall check.
|
|
4
4
|
*
|
|
5
5
|
* Blocks (exit 2) at most ONCE per session — never traps the user in a
|
|
6
|
-
* loop regardless of how Claude responds.
|
|
6
|
+
* loop regardless of how Claude responds. Both cases below require jtb's
|
|
7
|
+
* fetch to have actually run this session (backlog #15) — a ticket-key-
|
|
8
|
+
* shaped string appearing incidentally (a doc, a code comment, a test
|
|
9
|
+
* fixture) is not enough, matching SKILL.md's own capture-guidance scope.
|
|
10
|
+
* Given that:
|
|
7
11
|
* 1. Claude flagged something (🔖 Recall-flag:) but never called note add
|
|
8
12
|
* — a broken promise, the strongest signal something was missed.
|
|
9
13
|
* 2. Ticket work happened all session with zero flags and zero notes
|
|
10
14
|
* — the weaker "did anything ever get considered?" catch.
|
|
11
|
-
* Anything else (no
|
|
15
|
+
* Anything else (no fetch this session, or a note was already added) exits
|
|
12
16
|
* clean — this must never be the reason a session can't end.
|
|
13
17
|
*
|
|
14
18
|
* The per-session_id "asked once" state (readState/writeState) cannot
|
|
@@ -45,7 +49,7 @@ const cwd = input?.cwd ?? process.cwd();
|
|
|
45
49
|
|
|
46
50
|
if (!sessionId || !transcriptPath) process.exit(0);
|
|
47
51
|
|
|
48
|
-
const {
|
|
52
|
+
const { sawFetch, sawRecallFlag, sawNoteAdd, ticketKey } = scanTranscript(transcriptPath);
|
|
49
53
|
|
|
50
54
|
// Refreshed on every check, independent of the once-per-session gate below —
|
|
51
55
|
// a capture that happens AFTER this session already nagged once must still
|
|
@@ -58,7 +62,7 @@ if (state.stopChecked) process.exit(0); // already asked once this session — r
|
|
|
58
62
|
const profile = resolveProfile(ticketKey, { cwd });
|
|
59
63
|
const recallStrictness = normalizeRecallStrictness(profile?.recallStrictness);
|
|
60
64
|
|
|
61
|
-
if (!shouldNag({
|
|
65
|
+
if (!shouldNag({ sawFetch, sawRecallFlag, sawNoteAdd, recallStrictness })) {
|
|
62
66
|
process.exit(0); // nothing this strictness level requires a capture for
|
|
63
67
|
}
|
|
64
68
|
|