Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project are documented in this file.

### Fixed

- `check`/`watch`: fold curly apostrophes (U+2018/U+2019/U+02BC) and curly double quotes into ASCII before matching; the previous normalization was a no-op (#185)
- `watch`: run pattern matching on ANSI-stripped text so inline SGR codes no longer split phrases and hide detections (#186)
- `check`: read piped stdin to EOF instead of rejecting input that arrives more than 100 ms after start (#183)
- `watch`/`attach`: reject unknown `--mode` and `--rebuttal` values with exit 1 instead of silently running watch mode or never sending rebuttals (#176)
- `sightings --json` now prints `[]` instead of ANSI-colored prose when no sightings are recorded (#175)
Expand Down
5 changes: 5 additions & 0 deletions src/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ describe('check', () => {
// The CLI's colour tiers (cli.ts colorConfidence) assume the matcher's
// confidence floor. Pin that floor so a change to KEYWORD_WEIGHT or
// MIN_KEYWORD_CONFIDENCE has to revisit the tiers deliberately.
it('folds curly apostrophes into ASCII before matching', () => {
const result = check('that is another agent/team\u2019s job');
assert.strictEqual(result.matches[0]?.confidence, 1.0);
});

describe('confidence floor', () => {
// 1 * KEYWORD_WEIGHT + MIN_KEYWORD_CONFIDENCE; 0.15 + 0.3 is
// 0.44999999999999996 in IEEE doubles, so compare with a tolerance.
Expand Down
2 changes: 1 addition & 1 deletion src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const FILLER_WORDS = /\b(is|are|was|were|been|being|has|have|had|do|does|did|wil
const VERB_SUFFIXES = /\b(\w+?)(ing|ed|s)\b/g;

function normalizeText(text: string): string {
return text.toLowerCase().replace(/['']/g, "'").replace(/\s+/g, ' ').trim();
return text.toLowerCase().replace(/[\u2018\u2019\u02BC]/g, "'").replace(/[\u201C\u201D]/g, '"').replace(/\s+/g, ' ').trim();
}

function reduceText(text: string): string {
Expand Down
9 changes: 9 additions & 0 deletions src/watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ describe('event buffering and flushing', () => {
watcher.stop();
});

it('detects phrases split by inline ANSI SGR codes', () => {
const { watcher, detections } = makeWatcher();
const w = internals(watcher);
w.handleEvent(rawOutput('test-session', 'all \x1b[1mdone\x1b[0m / steady state'));
w.handleEvent(stateChange('test-session', 'working', 'idle'));
assert.strictEqual(detections.length, 1);
watcher.stop();
});

it('flushBuffer is a no-op when the buffer is empty', () => {
const { watcher, detections } = makeWatcher();
internals(watcher).flushBuffer();
Expand Down
7 changes: 3 additions & 4 deletions src/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,15 +269,14 @@ export class Watcher extends EventEmitter {
this.flushTimer = null;
}

// Only used to build the human-readable log preview below; pattern
// matching runs against the raw, unstripped `text`. Uses the shared
// escape-stripping helper from sanitize.ts so this cleanup can't drift
// Strip escapes before matching so inline SGR codes cannot split phrases.
// Uses the shared helper from sanitize.ts so this cleanup can't drift
// out of sync with the operator-facing print-safety boundary.
const stripped = stripTerminalEscapes(text, '').trim();
const preview = stripped.slice(0, 200).replace(/\n/g, ' ');
this.log(`flush #${this.flushCount}: checking ${lineCount} lines (${text.length} chars)`);
this.log(`flush #${this.flushCount}: text: "${preview}${stripped.length > 200 ? '...' : ''}"`);
const result = check(text, { excuses: this.excuses });
const result = check(stripped, { excuses: this.excuses });

if (result.clean) {
this.log(`flush #${this.flushCount}: clean`);
Expand Down
Loading