The jobindex and linkedin detail parsers matched description containers with a non-greedy regex that stops at the first inner </div>, so any posting whose description contains nested divs was silently truncated (jobindex dropped later sections; linkedin dropped everything after the first block). Replaces the regex with a depth-tracked extractDivContent scanner that walks div open/close markers to the matching close. Verified: truncation bug reproduced against real markup fixtures, depth arithmetic correct (no off-by-one/infinite-loop), 28 tests pass network-free, no regression on non-nested divs. Malformed-HTML over-grabs rather than truncates - the safer failure, cleaned by downstream stripTags/decode.
By @oscarbol09.
The freehire and linkedin CLIs called main().then(code => process.exit(code)) with no .catch(). If main() throws or returns a rejected promise, the .then() never runs, process.exit() is never called, and the runtime terminates with exit code 0 - a runtime failure becomes indistinguishable from success (and would suppress the Step 1c WebSearch fallback, which keys on non-zero exit). Adds a .catch() that writes the same JSON error shape the rest of each file already uses ({error, code: INTERNAL_ERROR} to stderr) and exits 1. Scoped to the two self-contained CLIs only; the bunli portals catch internally via defineCommand.
By @oscarbol09.
None of the board CLIs set a fetch timeout, and the retry loops react only to HTTP status codes, not to a connection that is accepted then never responds (black-holed TCP, hung TLS, stalled proxy) - so await fetch(...) never settles and the command hangs with no output and no exit. freehire's helper even documented a fast-degrade contract its try/catch didn't deliver on a mid-flight stall. Adds signal: AbortSignal.timeout(15000) to every fetch across all six CLIs, with network-free tests asserting the signal is present on each request wrapper.
By @thejesh23. Verified: 8 timeout tests pass locally with fetch stubbed (no network), and would fail on the pre-fix code.
Closes#196
Implements the mechanism approved in discussion #93: enabled: true|false frontmatter on portal skills (missing key = enabled), honored during /scrape portal discovery, with skipped portals reported visibly in the run summary.
- Untrack job_search_tracker.csv: it was both tracked and listed in
.gitignore (same inconsistency class as the settings.local.json fix
in #27). Users' personal rows risked merge conflicts on every pull;
commands already create the file with the standard header when it
is missing.
- Scope job-scraper's allowed-tools Bash entry (from #52) to
'bun --version' and the portal-CLI invocation pattern, adopting the
tighter form proposed in #65.
- Fix all five portal SKILL.mds documenting 'bun run skills/...'
paths that do not resolve from the repo root ('.agents/skills/...'
is correct) - now load-bearing since #52 wired /scrape to read
these docs for CLI invocations. Surfaced in #66.
- Teach tools/lint_skills.py to glob-expand allowed-tools bun run
targets so scoped wildcard permissions lint correctly.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
decodeHtmlEntities only handled decimal numeric character references
(é); the equally valid hexadecimal form (é) fell through
undecoded and surfaced as raw text in titles, companies, locations and
descriptions. It also used String.fromCharCode, which corrupts
supplementary-plane code points (e.g. emoji, U+1F600).
Add a hexadecimal numeric-entity rule and route both decimal and hex
through a fromCodePoint-based helper with a valid-range guard. Add
network-free unit tests covering hex, uppercase-X hex, decimal
(regression) and astral code points via the exported parse functions.
* fix: silent zero output in salary converter and NaN filter bypass in LinkedIn CLI
Bug #10 (convert_salary_excel.py):
openpyxl ws[row_index] random access fails silently under read_only=True,
leaving headers empty and producing no output. Fix: save the header row
values during the existing iter_rows scan so ws[header_row] is never called.
Bug #5 (.agents/skills/linkedin-search/cli/src/cli.ts):
parseInt on --jobage/--page/--limit flags returns NaN on non-numeric input.
NaN propagates silently — the jobage filter is dropped, page/limit are broken.
Fix: validate each parsed int, exit 1 with a structured BAD_ARG error on NaN.
Also adds:
- tests/test_bug10_salary_converter.py: 10 scenarios, 31 assertions (all green)
- tests/test_bug5_linkedin_cli.sh: 8 scenarios, 16 assertions (all green)
- docs/bugfixes.md: root cause, impact, and fix explanation for both bugs,
plus a note on the pre-existing detect_column_type "n" pattern issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address PR review — drop salary converter change, move test to bun
- Drop tools/convert_salary_excel.py change (bug not reproducible on
modern openpyxl; reviewer confirmed master works correctly)
- Drop tests/test_bug10_salary_converter.py and top-level tests/ dir
- Drop docs/bugfixes.md (analysis belongs in PR description, not repo)
- Replace tests/test_bug5_linkedin_cli.sh with a proper bun test file
at .agents/skills/linkedin-search/cli/tests/cli-flag-validation.test.ts
following the jobindex-search/cli/tests/ convention (runCLI/parseJSON
helpers, describe/test/expect, descriptive names)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
A general-purpose, field-agnostic job-search skill built on LinkedIn's public
jobs-guest endpoints. Works for any market out of the box — location is an
explicit required flag (no country default). Zero runtime dependencies (bun only);
search + detail commands.
Includes a personal-use / Terms-of-Service note (automated access is against
LinkedIn's ToS — keep volume low, non-commercial).
(Pairs with the .gitignore fix in #21, which lets skills under .agents/ be tracked.)
Co-authored-by: Akhil Tripathi <kodabear@Akhils-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>