ticketlens 0.42.2 → 0.42.4
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 +1 -0
- package/package.json +1 -1
- package/skills/jtb/hooks/recall-auto-capture.mjs +12 -3
- package/skills/jtb/hooks/recall-nudge-stop.mjs +26 -3
- package/skills/jtb/scripts/fetch-ticket.mjs +1 -1
- package/skills/jtb/scripts/lib/branch-scanner.mjs +1 -1
- package/skills/jtb/scripts/lib/requirement-extractor.mjs +12 -3
package/README.md
CHANGED
|
@@ -1121,6 +1121,7 @@ npm test
|
|
|
1121
1121
|
See [ROADMAP.md](ROADMAP.md) for the full plan.
|
|
1122
1122
|
|
|
1123
1123
|
Recently shipped:
|
|
1124
|
+
- **`review`/`compliance` fixes** — branch diff now uses merge-base semantics, no longer picks up unrelated unmerged-main files; AC parser now recognizes Jira wiki `Requirements`/`How to test` headers, not just literal "Acceptance Criteria"
|
|
1124
1125
|
- **`ticketlens update --priority`** — a bad priority name now tells you the project's real, current valid options instead of a bare tracker error
|
|
1125
1126
|
- **`ticketlens assign --to="name or email"`** — assign a ticket to another developer, not just yourself. Jira (Cloud + Server/DC); searches real assignable users first, lists matches, executes only on one confirmed match. Pro tier
|
|
1126
1127
|
- **Console: Recall "select all N matching" bulk delete** — Gmail-style banner deletes every note matching your search/filters across all pages, not just the current one
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ import { appendFileSync } from 'node:fs';
|
|
|
29
29
|
import { join } from 'node:path';
|
|
30
30
|
import { isLicensed } from '../scripts/lib/license.mjs';
|
|
31
31
|
import { readCliToken } from '../scripts/lib/cli-auth.mjs';
|
|
32
|
-
import { buildCaptureExcerpt, privateTmpDir } from './recall-nudge-lib.mjs';
|
|
32
|
+
import { buildCaptureExcerpt, privateTmpDir, writeLastCaptureAt } from './recall-nudge-lib.mjs';
|
|
33
33
|
import { autoCapture } from '../scripts/lib/summarizer.mjs';
|
|
34
34
|
import { runNoteAdd } from '../scripts/lib/note-command.mjs';
|
|
35
35
|
import { DEFAULT_CONFIG_DIR } from '../scripts/lib/config.mjs';
|
|
@@ -56,17 +56,20 @@ function logLine(message) {
|
|
|
56
56
|
* @param {string} opts.transcriptPath
|
|
57
57
|
* @param {string} [opts.ticketKey]
|
|
58
58
|
* @param {string} [opts.configDir]
|
|
59
|
+
* @param {string} [opts.cwd] - directory whose shared capture marker gets updated on a real capture (backlog #44: makes a successful background capture visible to a later sync Stop check, which otherwise has no way to know this ran)
|
|
59
60
|
* @returns {Promise<{outcome: 'skipped'|'captured'|'not-written'|'error', reason?: string, title?: string, error?: string}>}
|
|
60
61
|
*/
|
|
61
62
|
export async function runAutoCapture({
|
|
62
63
|
transcriptPath,
|
|
63
64
|
ticketKey,
|
|
64
65
|
configDir = DEFAULT_CONFIG_DIR,
|
|
66
|
+
cwd = process.cwd(),
|
|
65
67
|
isLicensedFn = isLicensed,
|
|
66
68
|
readCliTokenFn = readCliToken,
|
|
67
69
|
buildCaptureExcerptFn = buildCaptureExcerpt,
|
|
68
70
|
autoCaptureFn = autoCapture,
|
|
69
71
|
runNoteAddFn = runNoteAdd,
|
|
72
|
+
writeLastCaptureAtFn = writeLastCaptureAt,
|
|
70
73
|
} = {}) {
|
|
71
74
|
if (!isLicensedFn('pro', configDir)) {
|
|
72
75
|
logLine('skipped: not licensed');
|
|
@@ -112,10 +115,16 @@ export async function runAutoCapture({
|
|
|
112
115
|
});
|
|
113
116
|
|
|
114
117
|
logLine(written ? `captured: ${result.title}` : `not-written: ${result.title}`);
|
|
118
|
+
if (written) writeLastCaptureAtFn(cwd, Date.now());
|
|
115
119
|
return written ? { outcome: 'captured', title: result.title } : { outcome: 'not-written', title: result.title };
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
// cwdArg (backlog #44 code review): passed explicitly by the spawning Stop
|
|
124
|
+
// hook rather than relying on this child's own process.cwd() — Node
|
|
125
|
+
// resolves process.cwd() through symlinks, so a cwd with a symlinked
|
|
126
|
+
// component would otherwise hash to a different marker file than the one
|
|
127
|
+
// a later Stop's hasRecentCapture(cwd) reads, silently breaking the bridge.
|
|
128
|
+
const [, , transcriptPath, ticketKeyArg, cwdArg] = process.argv;
|
|
129
|
+
runAutoCapture({ transcriptPath, ticketKey: ticketKeyArg || undefined, cwd: cwdArg || undefined }).catch(() => { /* never escape — nothing is watching this process */ });
|
|
121
130
|
}
|
|
@@ -89,11 +89,31 @@ if (!sessionId || !transcriptPath) process.exit(0);
|
|
|
89
89
|
|
|
90
90
|
const { sawFetch, sawMutatingAction, sawRecallFlag, sawNoteAdd, ticketKey } = scanTranscript(transcriptPath);
|
|
91
91
|
const cliToken = readCliToken();
|
|
92
|
+
// Backlog #44 (9th/10th reports): entitled accounts already run the async
|
|
93
|
+
// auto-capture judge below — throttled to at most once per CAPTURE_FRESHNESS_MS
|
|
94
|
+
// per cwd (hasRecentAutoCaptureAttempt below), not on every single Stop. Once
|
|
95
|
+
// it succeeds, its outcome is now visible via writeLastCaptureAt
|
|
96
|
+
// (recall-auto-capture.mjs) to a LATER Stop's hasRecentCapture() check — but
|
|
97
|
+
// THIS Stop's own decision below still can't see an attempt spawned moments
|
|
98
|
+
// ago (detached, still running), nor is a fresh one spawned if the window's
|
|
99
|
+
// one attempt already ran and moved on. For that residual window, a hard
|
|
100
|
+
// block duplicates a safety net that's already in flight or already spent:
|
|
101
|
+
// exit 0 instead of 2 so this never blocks the turn for these accounts. Free
|
|
102
|
+
// tier has no such net, so it keeps the hard block unchanged — this is its
|
|
103
|
+
// only enforcement.
|
|
104
|
+
const hasSafetyNet = isLicensed('pro') && Boolean(cliToken);
|
|
92
105
|
|
|
93
|
-
if (
|
|
106
|
+
if (hasSafetyNet && !hasRecentAutoCaptureAttempt(cwd)) {
|
|
94
107
|
try {
|
|
95
108
|
writeLastAutoCaptureAttemptAt(cwd, Date.now());
|
|
96
|
-
|
|
109
|
+
// cwd is passed explicitly as argv (not left to the child's own
|
|
110
|
+
// process.cwd()) — code review finding: Node resolves process.cwd()
|
|
111
|
+
// through symlinks, so a cwd with any symlink component (macOS os.tmpdir()
|
|
112
|
+
// itself, an iCloud-synced Desktop, a Docker bind mount) would make the
|
|
113
|
+
// child's own process.cwd() differ from the string used here, hashing to
|
|
114
|
+
// a different marker file and silently breaking the bridge this fix exists
|
|
115
|
+
// to build.
|
|
116
|
+
const child = spawn(process.execPath, [AUTO_CAPTURE_SCRIPT, transcriptPath, ticketKey ?? '', cwd], {
|
|
97
117
|
cwd,
|
|
98
118
|
detached: true,
|
|
99
119
|
stdio: 'ignore',
|
|
@@ -156,4 +176,7 @@ if (sawRecallFlag) {
|
|
|
156
176
|
'`ticketlens note add`. If genuinely nothing qualified, just say so — then finish.\n',
|
|
157
177
|
);
|
|
158
178
|
}
|
|
159
|
-
|
|
179
|
+
// hasSafetyNet: the message above is still written (a diagnostic trail if
|
|
180
|
+
// the async job silently fails) but exit 0 means Claude Code never surfaces
|
|
181
|
+
// it or blocks the turn — only exit 2 feeds stderr back (backlog #44).
|
|
182
|
+
process.exit(hasSafetyNet ? 0 : 2);
|
|
@@ -800,7 +800,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
800
800
|
const logResult = execFn('git', ['log', '--oneline', `${baseBranch}..HEAD`], { encoding: 'utf8', cwd, timeout: 10_000 });
|
|
801
801
|
const commitMessages = logResult.status === 0 ? (logResult.stdout ?? '') : '';
|
|
802
802
|
|
|
803
|
-
const diffResult = execFn('git', ['diff', `${baseBranch}
|
|
803
|
+
const diffResult = execFn('git', ['diff', `${baseBranch}...HEAD`], { encoding: 'utf8', cwd, timeout: 30_000 });
|
|
804
804
|
const diff = diffResult.status === 0 ? (diffResult.stdout || null) : null;
|
|
805
805
|
|
|
806
806
|
const allText = [headBranch ?? '', commitMessages].join('\n');
|
|
@@ -43,7 +43,7 @@ export function scanCurrentBranch({ cwd = process.cwd(), execFn = spawnSync, fsC
|
|
|
43
43
|
|
|
44
44
|
let files = [];
|
|
45
45
|
if (base) {
|
|
46
|
-
const diffOut = run(execFn, ['diff', '--name-only', `${base}
|
|
46
|
+
const diffOut = run(execFn, ['diff', '--name-only', `${base}...HEAD`], cwd);
|
|
47
47
|
if (diffOut) {
|
|
48
48
|
files = diffOut.trim().split('\n').filter(Boolean).slice(0, MAX_FILES);
|
|
49
49
|
}
|
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
const RE_GWT = /^\s*(given|when|then)\s+(.+)/i;
|
|
10
10
|
const RE_MUST_ITEM = /^\s*[-*•]\s+((?:must|should|shall|ensure|verify)\b.+|.+(?:must|should|shall|ensure|verify).+)/i;
|
|
11
11
|
const RE_NUM_MUST = /^\s*\d+[.)]\s+((?:must|should|shall|ensure|verify)\b.+|.+(?:must|should|shall|ensure|verify).+)/i;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Matches "Acceptance Criteria" plus the two real-world Jira Server/DC section headers
|
|
13
|
+
// (backlog #43) that carry the same content without that literal phrase: "Requirements"
|
|
14
|
+
// and "How to test". A nested subheading (e.g. "h3. Validation range") does NOT match
|
|
15
|
+
// this and still exits the section — known limitation, filed as a follow-up.
|
|
16
|
+
const RE_AC_HEADER = /^\s*(?:#+\s*|h[1-6]\.\s*)?(?:acceptance criteria|requirements|how to test)\s*:?\s*$/i;
|
|
17
|
+
// Wiki-only (h1.-h6.) for section-exit detection — backlog #43: a leading markdown "#"
|
|
18
|
+
// is indistinguishable from Jira Server/DC's "#" numbered-list marker (e.g. "# Run the
|
|
19
|
+
// test suite."), so including it here closed real AC/how-to-test sections on their own
|
|
20
|
+
// first list item. Entry detection (RE_AC_HEADER/RE_NON_AC_HEADER) is unaffected — those
|
|
21
|
+
// still accept a markdown "#" prefix on the header line itself.
|
|
22
|
+
const RE_HEADING = /^\s*h[1-6]\.\s+/i;
|
|
14
23
|
const RE_BULLET = /^\s*[-*•]\s+(.+)/;
|
|
15
24
|
const RE_NUM_ITEM = /^\s*\d+[.)]\s+(.+)/;
|
|
16
25
|
const RE_NUM_MARKER = /^\s*(\d+)[.)]/;
|
|
@@ -101,7 +110,7 @@ export function extractRequirements(text) {
|
|
|
101
110
|
|
|
102
111
|
if (RE_AC_HEADER.test(line)) { inAcSection = true; continue; }
|
|
103
112
|
|
|
104
|
-
// Exit on any explicit heading (
|
|
113
|
+
// Exit on any explicit heading (wiki h1.-h6. only, see RE_HEADING) that is not the AC header
|
|
105
114
|
if (inAcSection && RE_HEADING.test(line) && !RE_AC_HEADER.test(line)) {
|
|
106
115
|
inAcSection = false;
|
|
107
116
|
}
|