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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.38.33",
3
+ "version": "0.38.34",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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({ sawTicketKey, sawRecallFlag, sawNoteAdd, recallStrictness = 'balanced' }) {
240
- if (!sawTicketKey || sawNoteAdd) return false;
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: ticket work with no note is enough
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. Two cases force a check:
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 ticket work at all, or a note was already added) exits
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 { sawTicketKey, sawRecallFlag, sawNoteAdd, ticketKey } = scanTranscript(transcriptPath);
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({ sawTicketKey, sawRecallFlag, sawNoteAdd, recallStrictness })) {
65
+ if (!shouldNag({ sawFetch, sawRecallFlag, sawNoteAdd, recallStrictness })) {
62
66
  process.exit(0); // nothing this strictness level requires a capture for
63
67
  }
64
68