| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(evals): keep text before the first chapter heading reachable (#286) The PD-03 paper-flat builder sliced chunks from the first matched heading line, so a preface, abstract, or held-out constraint ahead of the first heading was silently dropped from every emitted RAW chunk. Fold that prefix into the first chunk instead of discarding it. The chunk count is unchanged, so supplied metadata still lines up. | 7 天前 | |
fix(extractor): translate post-extraction stat errors (#228) * fix(extractor): translate post-extraction stat errors * test: use one utils import style | 30 天前 | |
fix(extractor): resolve Persian ZWNJ ordinals after sanitization (#287) | 7 天前 | |
fix(extractor): stop the Calibre path reusing a previous source's output (#229) * fix(extractor): stop the Calibre path reusing a previous source's output extract_with_ebook_convert() wrote to a fixed filename in the run's work directory and treated "exit code 0 and the file exists" as success. The work directory is per-process and shared by every source in a batch, so a conversion that reports success without writing anything was satisfied by a file an earlier source had left behind: one book's text returned for another source, and recorded under that source's name in metadata.json. Each call now gets its own output filename, so the existence check can only be true for a file this call wrote. * fix(extractor): convert into a per-call TemporaryDirectory under OUTPUT_DIR Review feedback on the pid+counter name: a reused pid in a second interpreter sharing BOOK_SKILL_WORKDIR collapses back to the first run's name, so the second conversion exits 0 without writing and still returns the first process's text. Each call now allocates a fresh TemporaryDirectory inside OUTPUT_DIR — unaddressable from outside the call — and reads the text before cleanup. Removes the per-source intermediate leftovers as a side effect. A directory that cannot be created is raised (with the path and cause) rather than returned as None: 'could not create' and 'converted fine but wrote nothing' must not look identical, which is the silent-failure shape this function exists to refuse. Adds test_reused_workdir_with_same_pid_across_interpreters (the reviewer's scenario) and test_unwritable_output_dir_raises. * fix(extractor): raise ExtractionError for consistency with other parsers A bare OSError from the temp-directory creation would bypass the batch tolerance path in utils.py, which catches ExtractionError per source (non-fatal in batch mode) and lets other files continue. Every other parser already signals extraction failures through ExtractionError, so an OSError here could abort a whole batch instead of skipping one file. No behavior change on the success path; only the exception type moves. The original OSError stays chained via `raise ... from e`, and the regression test now asserts ExtractionError. --------- Co-authored-by: Ubuntu <blrain@blrain.jaugt1xmj4bezacssydzfivrnc.lx.internal.cloudapp.net> | 19 天前 | |
fix(extract): report which method produced the chapter count (#150) detect_structure() picks between counting numeric "Chapter N" headings and falling back to structural Markdown headings, and said nothing about which one answered. Every parser in this project already announces its method ("Trying python-docx... OK", "[warn] extract_with_pdftotext failed") — this decision had the same shape and was the only silent one. The cost is not cosmetic. chapters_detected becomes Step 3's plan and the generated skill's chapter files, so a wrong count does not look wrong; it becomes the artifact. In the conversion that prompted this, a 26-page paper with 8 numbered sections reported 4 and the user only found out by reading the headings himself. Adds chapters_method (numeric / structural / none) to the structure dict, prints it per source and in the final summary, and records it in metadata.json next to the count, so a batch run can be audited afterwards rather than watched live. Also warns when the structural fallback returns one section for a document over 5,000 words: that pairing is a detection failure far more often than a one-chapter book, and it is exactly the case that was silent before. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(extractor): exclude framing headings from structural chapters (#253) | 12 天前 | |
fix(sanitize): fold Kangxi radicals to their ideographs in extracted text (#277) A PDF whose embedded font subset carries no correct Unicode mapping emits a Kangxi radical where a character belongs, so a Chinese book extracts "判断⼒" for "判断力". Nothing looks wrong on the page and the run reports success, so the corruption reaches full_text.txt, every grep, the chapter-heading match and the topic index. Each code point in U+2F00-U+2FDF has a Unicode compatibility decomposition, so the mapping is NFKC restricted to that block rather than a hand-written table. Nothing is removed, so the fold is kept out of the invisible removal count the CLI reports as a security strip. CJK Radicals Supplement (U+2E80-U+2EF3) is deliberately left alone: Unicode defines no decomposition for it, and some entries are ordinary kanji (U+2EEB is 匀, not a radical form of 二). Fixes #274 | 6 天前 | |
fix(extract): count supplementary-plane CJK in the token estimate (#136) _CJK_RE covered only the BMP, so Unified Ideographs Extension B and up fell through to the whitespace-word branch — a space-less run of them counted as a single word. 2,000 characters of Ext B/C/D/E estimated at 1 token instead of 1,333: the same ~1000x undercount #103 fixed, one plane up. The estimate feeds the Step 5 cost pre-flight, and under-reporting is the harmful direction — it invites a run that costs far more than quoted. Takes Planes 2 and 3 end to end (U+20000-U+3FFFF) rather than enumerating blocks. The first draft stopped at the end of Extension G and silently left Extension H (U+31350-U+323AF, 4,192 assigned ideographs) still broken; the wider range cannot rot that way. Emoji, mathematical alphanumerics and regional indicators are all in Plane 1 and stay on the word branch, pinned by tests. | 2 个月前 | |
fix(extractor): validate Markdown code fence closers (#291) * fix(extractor): validate Markdown code fence closers * test(extractor): make fence regressions runnable in isolation | 7 天前 | |
fix(extractor): preserve corpus newlines on Windows (#264) | 12 天前 | |
fix(deps): honor alternative parser availability (#208) | 1 个月前 | |
fix(discovery_tax): reuse the extractor's multilingual ToC detection (#138) discovery_tax reused the extractor's _chapter_number 'so discovery_tax and the pipeline always agree', then duplicated ToC detection with a stale local regex matching EN/ES/PT only. On a French, German, Italian, Dutch or CJK book it missed the ToC and fell back to counting the whole front matter, inflating the modelled discovery-loop cost that backs the README's token-savings claim. Now imports _TOC_PATTERN alongside _chapter_number. The swap was not purely additive — the old local regex matched sum[áa]rio, both accented and not, while _TOC_HEADERS carried only 'sumário' — so unaccented 'sumario' is added upstream in utils.py, where the pipeline gains it too. One detector, one answer, in both directions. | 2 个月前 | |
fix(pdf): report Docling fallback in technical mode (#255) | 7 天前 | |
docs: add Codex to host lists (#278) | 7 天前 | |
fix(epub): report unextracted source images (#134) * fix(epub): report unextracted source images * fix(epub): keep small image sets quiet --------- Co-authored-by: Bryan Nathan <bryan@users.noreply.github.com> | 1 个月前 | |
fix(resume): preserve duplicate source multiplicity (#267) * fix(resume): preserve duplicate source multiplicity * fix(resume): rebuild corpus when sources reorder | 7 天前 | |
feat(hosts): add Hermes Agent skill discovery and validation (#202) * feat(hosts): add Hermes Agent support * fix(hosts): honor Hermes project trust and precedence * fix(hosts): require Git root for project discovery --------- Co-authored-by: Douglas Ingram <douglas.ingram@outlook.com> | 1 个月前 | |
fix(html): emit block boundaries at end tags in the stdlib parser (#107) _HTMLTextExtractor only appended "\n" on a block element's OPENING tag, and its tag list omitted every table and definition-list element. Text from adjacent blocks therefore concatenated with no separator. Table cells were the worst case -- an entire row collapsed into one token run: before: '200OKRequest succeeded' after : '200\tOK\tRequest succeeded' On a 5-row HTTP status-code reference table, 7 of 7 merged-token probes were present before and 0 after; the whitespace-token count goes from 21 to the correct 31. Where a heading is followed directly by text or by an unlisted element ("<h2>Chapter 1</h2>Introduction", or "</h2><section>"), the merge also destroys chapter detection: _EXPLICIT_CHAPTER requires a word boundary after the number and "1I" has none, so the heading is silently not counted. Four such layouts went from 0 detected chapters to the correct 3; the bs4 path already reported 3, so this also closes a divergence between the two paths. This is the dependency-free fallback -- the path CI's own smoke job exercises -- and it is shared by the stdlib EPUB extractor. Boundaries are now emitted on both open and close. They are deferred until the next non-blank text run, so nested blocks collapse to a single separator instead of a run of blank lines, and there is no leading blank line. Cells join with "\t" (the convention the stdlib DOCX fallback already uses for table rows) so a row stays on one line and a table-formatted ToC entry remains parseable as a heading. Inline elements are deliberately untouched: "hyper<span>text</span>" must stay one word. | 2 个月前 | |
fix(extractor): use trafilatura for real boilerplate detection in HTML (#142) * docs: add BUILDLOG.md for our contribution work on this fork * docs: add PR #142 entry, update PR #99 to MERGED status Real correction included: BUILDLOG.md appeared missing between 08-11 and 08-12, but was never actually lost -- this clone's local master had drifted to track origin/master (upstream) instead of fork/master (ours) after a direct git fetch, making the real file look orphaned when it was safe on fork/master the whole time. * docs: log PR #142 CI break-and-fix (trafilatura missing in CI, PR checklist box) * fix(extractor): use trafilatura for real boilerplate detection in HTML extract_html_content() only stripped <script>/<style>/<head> on both the bs4 and stdlib-fallback paths -- neither does actual main-content vs. page-chrome detection. Verified against 61 real pages scraped from personalmba.com (two full chapters): a repeated footer block (ad + author bio + copyright notice) appeared 183 times in the combined extracted text. Adds trafilatura as the new primary extraction path, falling through to the existing bs4 path and then the stdlib parser on any failure (missing dependency, low-quality/whitespace result, or a parse exception) -- same graceful-degradation chain the function already used. Kept as an optional dependency (pip install book-to-skill[html]) so the base install stays dependency-free. Re-verified against the same 61 pages: repeated boilerplate dropped from 183 to 69 hits. The footer (ad block, copyright/trademark notice) is fully eliminated; the remaining 69 are one line -- the site's own page masthead/tagline, appearing once per page, which sits structurally like real article lead-in text rather than nav/footer chrome. * fix(tests): skip trafilatura-dependent tests when trafilatura isn't installed CI installs only pytest (no package extras), so the 4 tests that patch("trafilatura.extract", ...) failed with ModuleNotFoundError on every Python version in the matrix -- mock.patch needs the real module importable to resolve the patch target. trafilatura is an optional extra (book-to-skill[html]); the base CI tier is deliberately dependency-free. Guard each with pytest.importorskip("trafilatura"), matching the existing bs4 skip idiom in test_html_block_boundaries.py. * docs+fix(html): register trafilatura in DEPENDENCY_GROUPS, print which extractor answered, document [html] extra weight Addresses maintainer review on #142: - --check previously reported HTML as bs4-only and never mentioned trafilatura or whether it was installed. Registered as the primary module in the HTML dependency group. - extract_html_content() silently returned one of two materially different documents (trafilatura strips page chrome, bs4/stdlib don't) with no indication which ran. Prints now mirror extract_docx()'s "Trying X..." style. - docs/install.md never mentioned the [html] extra pulls trafilatura's 17-package HTML-processing stack (lxml, date/timezone/URL parsing). Added a one-line note under the pip install example. * docs(buildlog): log the #142 review round -- two required fixes, question answered, upstream-vs-fork rebase gotcha * chore: address final review round on PR #142 Drop BUILDLOG.md from the branch -- it's a personal working log (local paths, internal vault references), not something upstream should have to maintain. Remove the meaningless "ponytail:" label from the html.py comment, keeping the sentence it was attached to. Per maintainer's review: "With those two out, this is ready." * chore: remove stray ponytail label from html.py comment (missed in previous commit) | 1 个月前 | |
feat(cli): replace the ASCII banner with two quiet lines, and fund the project at the end (#153) The 24-line braille-art banner opened every run. It did its job — the project is recognisable in videos people record — but it spends the most attention the tool ever gets on decoration, at the moment the reader has received nothing yet. Start of a run: two lines naming the project, the licence, and that it is maintained in personal time. No ask — nothing has been delivered yet. End of a successful run: one two-line note about funding, after the report, where the reader has just been handed extracted text and a structure they can use. It states what the money is for instead of asking for it. The note never prints when extraction failed. Nobody should be asked to fund a tool that just wasted their time. Written to stdout with the rest of the closing report: stderr is unbuffered and stdout is not when the run is piped, which is how an agent captures it, so mixing the two puts the closing line at the top of the output. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(deps): diagnose a module installed in an isolated environment (#151) --check reports "✗ python: docling" on a machine where Docling is installed via pipx — the way its own documentation suggests installing it. The message is correct and useless: the module really is not importable from this interpreter, and the user really did install it. One reader spent a session reinstalling before finding that running the extractor with the pipx venv's own Python works. When a module is absent but a same-named executable is on PATH, the report now names it and points at the interpreter that can import it. The ✗ stays and the dependency is still counted as missing: the parsers import Docling, so a binary on PATH does not make the import work, and treating it as proof of installation would turn a cheap preflight message into a crash mid-run. Detection is one shutil.which() call, already the pattern used here for system tools. The venv interpreter is best-effort — pipx's <venv>/bin/<tool> layout puts a usable `python` next to the executable — and the line is omitted when there is no such sibling. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(text): reject malformed BOM-encoded input (#292) * fix(text): reject malformed BOM-encoded input * test(text): cover malformed BOM rejection Add regression tests for handling malformed BOM-encoded text documents. * test(text): use module import for extractor | 7 天前 | |
fix(extract): write metadata.json as UTF-8 (#105) metadata.json is dumped with ensure_ascii=False but written without an explicit encoding, so write_text() falls back to the locale encoding. Any non-ASCII chapter heading, filename or path then raises UnicodeEncodeError under Windows cp1252 or LC_ALL=C. This is the only unencoded text write in the package; the full_text.txt write two lines above already passes encoding="utf-8". The crash fires after every source has been extracted and after full_text.txt is written, leaving the workdir half-built with a stale metadata.json from a previous run -- the file the agent reads in Step 3. Repro (Windows, locale cp1252, Python 3.14, 2-chapter Chinese Markdown): File "book_to_skill/utils.py", line 713, in main OUTPUT_META.write_text(json.dumps(metadata, indent=2, ensure_ascii=False)) UnicodeEncodeError: 'charmap' codec can't encode characters in position 1265-1267: character maps to <undefined> Tests: TestMetadataOutputEncoding asserts the write declares utf-8 (platform-independent -- on a UTF-8 host the locale default happens to work, so only the declared encoding proves the fix) and that CJK headings round-trip through metadata.json. | 2 个月前 | |
fix(extractor): count structural chapters per source (#273) | 7 天前 | |
fix(extract): detect a table of contents in any source, not just the first (#114) detect_structure scans only the first ~30,000 characters for a ToC header, which is correct for one book -- a table of contents lives in the front matter. But the consolidated value was re-derived by running that same scan over the JOINED corpus, so the window only ever covered the first source. A ToC in any later book was invisible, and the same two books fed in the opposite order produced the opposite answer: book1 (56 KB, no ToC) + book2 (has ToC) -> has_toc = False (wrong) book2 (has ToC) + book1 (56 KB, no ToC) -> has_toc = True metadata.json was self-contradicting in the first case: sources[1].has_toc was True while the top-level has_toc was False. main() also prints its "No table of contents detected -- chapter mapping in Step 3 will rely on heading scan only" warning off the top-level value, so the agent was told to distrust a ToC that existed. has_toc is a per-source property, so it is now combined per source instead of re-derived from the corpus: each per-source result already scanned its own front matter, so OR them. Same shape as the #83 fix, which computes the consolidated chapter count from source texts rather than from full_text.txt. Single-source behavior is unchanged -- any() over one source is that source's own value. | 2 个月前 | |
chore(skill): keep __pycache__ out of deployed skill directories (#222) * fix(skill): keep __pycache__ out of deployed skill directories When book-to-skill is installed as an agent skill, the skill *is* the source tree: it is cloned or unpacked into a host skills root (e.g. `~/.config/opencode/skills/book-to-skill/`) and executed from there. Running any tool therefore writes `__pycache__/*.pyc` *inside the skill*, turning a 27-file install into 43 files of payload — noise in diffs, extra surface to scan, and bytecode shipped wherever the skill is published or shared. The guard has to live in the executable entry points, not at module import. `book_to_skill/utils.py` is a pure library module (imported by the package, the CLI and the tests); assigning `sys.dont_write_bytecode` at its import time silently mutates any embedding process that merely imports the package. So the policy is now wrapped in `if __name__ == "__main__":` — false for an embedding import — immediately after `import sys` and BEFORE `book_to_skill` is imported, in the four modules that are actually run directly: scripts/extract.py Step 2 extraction tools/scan_generated_skill.py Step 9.5 mandatory security scan tools/discovery_tax.py discovery-loop measurement book_to_skill/cli.py package CLI, runnable directly The import-time assignment in `book_to_skill/utils.py` is deleted outright. An agent invoking the skill does not set PYTHONDONTWRITEBYTECODE, so the guard has to live in the code rather than in the caller's environment. Two things that do NOT work, both measured: * Putting the guard in `book_to_skill/__init__.py` — Python compiles `__init__.py` before executing its body, so `__init__.cpython-*.pyc` is still emitted (27 -> 28 files). * `python -m book_to_skill` cannot be protected at all, for the same reason. It is not a documented path: SKILL.md invokes the extractor as `"$PYTHON_BIN" "$SCRIPT_PATH"`, never via `-m`. Adds tests/test_no_bytecode_pollution.py. It copies the skill payload into an isolated deployment (never the developer's tree), strips PYTHONDONTWRITEBYTECODE and PYTHONPYCACHEPREFIX, runs each entry point with `--help`, and asserts both that it exited 0 with its expected banner/usage on stdout/stderr and that no `.pyc`/`__pycache__` appeared. The old destructive `_clean()` helper, which deleted bytecode from the developer's checkout, is gone. A guard test diffs the discovered entry-point set against an explicit list, so a new invocable module fails the suite instead of silently shipping bytecode. A fourth test imports the package in a subprocess with `sys.dont_write_bytecode` pre-set to False and asserts it is unchanged, which is the direct regression guard for the import-time side effect removed here. Verified RED before the fix (entry points reporting "left build artifacts" and "no __main__ guard") and GREEN after, with a manual negative control: temporarily making an entry point raise SystemExit(1) fails the suite. * test(skill): prove the no-bytecode suite can fail The regression test could pass vacuously: it discarded the entry point's return code and asserted only that no bytecode files appeared, so an entry point that immediately raised SystemExit(1) still gave "1 passed". The old "the import really happened" probe ran in a *separate* subprocess, so it could not prove the invocation under test worked either. Add `test_negative_control_broken_entry_point_is_reported_as_failure`: it builds a deliberately broken entry point (body immediately `raise SystemExit(1)`, producing no output) and runs it through the *same* `_run_entry` helper the real tests use, asserting the helper reports failure. Because that helper checks both `returncode == 0` and the presence of the tool's expected output, this test locking in an `AssertionError` is the proof that "returncode is discarded" is no longer possible for any entry point. Demonstrated by temporarily breaking a real entry point: the suite fails with "scripts/extract.py exited 1; it must run successfully (--help)" and passes again once restored. * test(skill): avoid implicit string concatenation in import probe CodeQL flagged the list of implicitly concatenated string literals as a possible missing comma. Use explicit newlines in a single parenthesised string instead, which reads the same and removes the warning. --------- Co-authored-by: wgm66 <wgm66@users.noreply.github.com> | 25 天前 | |
fix(extractor): count numbered headings as chapters when they carry a chapter's weight (#149) _structural_chapter_count() rejected every ATX title starting with a digit, a guard meant to keep "## 5 Setup" from counting as a chapter. It also dropped every arabic-numbered heading, which is how papers, reports and standards are written — and what --mode technical with Docling produces. A 26-page paper with 8 numbered sections reported 4; in isolation the same shape reports 1. chapters_detected feeds Step 3's plan and the generated skill's file layout, so the wrong number does not look wrong, it becomes the structure. Digit-led titles are now held back and judged per depth: they count as chapters when there are at least three of them and their median body text reaches a chapter's weight. Both signals are required because neither separates the shapes alone — a three-step tutorial is systematic too, and one long section is not a numbering scheme. The numbers themselves are deliberately ignored. An ascending run starting at 1 describes "Step 1 / Step 2 / Step 3" as well as it describes a paper, and requiring an unbroken run would discard a book whose extraction lost a heading, a chapter list starting at 0, or a multi-source corpus where the numbering restarts — all three are covered by tests. The floor sits between measured medians of body text per section: tutorial steps ~20 chars, documentation sections ~500, paper sections ~2,000, real book chapters ~5,000. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
feat(hosts): add OpenClaw skill discovery and validation (#209) * feat(hosts): add OpenClaw skill discovery and validation - Add OpenClaw lens to tools/validate_skill.py - Update SKILL.md philosophy, Skill Locations, extractor probe, Step 5 destination table and reload guidance - Add OpenClaw install docs (managed ~/.openclaw/skills and cross-agent ~/.agents/skills shared path) - Update architecture/how-it-works/index and README i18n taglines Closes #205 * docs: include OpenClaw in host selection * test: skip POSIX probe checks on Windows --------- Co-authored-by: hossein <hossein@example.com> Co-authored-by: Hossein Rahimi <HoosseinRahimi@users.noreply.github.com> | 30 天前 | |
feat(hosts): add Opencode skill discovery and validation (#216) * feat(hosts): add Opencode skill discovery and validation Adds OpenCode (sst/opencode) as a supported skill host, following the same shape as the Hermes Agent host: a validator lens, discovery paths in the converter spec, and tests for both. Validation (`tools/validate_skill.py --lens opencode`): - name must match the official pattern `^[a-z0-9]+(-[a-z0-9]+)*$` (lowercase alphanumeric with single hyphen separators) — stricter than the general Agent Skills default, so dots and underscores are rejected - only the five documented frontmatter keys are recognized: name, description, license, compatibility, metadata - no `allowed-tools` enforcement: OpenCode grants access through `permission.skill` in opencode.json, not frontmatter - no description soft limit; only the 1024-char hard cap applies Discovery (SKILL.md): - personal roots: ~/.config/opencode/skills (OpenCode-managed), plus the ~/.agents/skills and ~/.claude/skills compatibility roots - project roots: .opencode/skills, .agents/skills, .claude/skills - no trust gate, unlike Hermes: OpenCode discovers skills by walking up from the working directory to the git worktree Tests: - 11 `test_opencode_lens_*` cases in tests/test_validate_skill.py - tests/test_opencode_host_support.py, symmetric to the Hermes suite minus its trust-gate cases (OpenCode has no such model); guarded by a real bash probe so it skips instead of failing on hosts where the WSL stub makes `bash` unusable Docs: install.md, architecture.md, how-it-works.md, index.md and the three READMEs list OpenCode alongside the existing hosts. The README repository map also listed only three lenses; it now lists all five. Refs: https://opencode.ai/docs/skills * test(opencode): probe project layout from the project root The `project-opencode` case failed on CI (Linux) while passing locally, because the new host suite is skipped on hosts without a working bash — so the guard hid a real bug until CI ran it. The spec's extractor probe lists project-local roots other than Hermes' as CWD-relative paths (`.opencode/skills/...`, alongside `.github/`, `.claude/`, `.agents/`); only the Hermes block rewrites candidates against `$PROJECT_ROOT`. The test ran the project layout from `project/src/nested`, where no `.opencode/` exists, so `SCRIPT_PATH` stayed empty and the assertion compared the CWD against the extractor. Run project layouts from the project root and keep the personal layouts at a nested CWD, which is what actually demonstrates that personal roots resolve as absolute `$HOME/...` paths independent of depth. Verified under a real bash: the project case now resolves, and the three personal cases (`.config/opencode`, `.agents`, `.claude`) still resolve from `project/src/nested`. * fix(hosts): resolve project-local skills from the git worktree Project-local candidates were listed as bare CWD-relative paths (`.opencode/skills/...`, `.github/...`, `.claude/...`, `.agents/...`), so the converter was only found when the agent happened to sit at the project root. From a nested directory such as `project/src/nested` the probe returned nothing and Step 2 failed with "Could not find scripts/extract.py". That contradicts how the hosts behave: OpenCode walks up from the working directory to the git worktree, so project skills are meant to be reachable from anywhere inside the project. Prepend the same four roots resolved against `$PROJECT_ROOT` (already computed by the probe) when it is non-empty, keeping the CWD-relative forms afterwards so behaviour outside a git repository is unchanged: ```bash if [ -n "$PROJECT_ROOT" ]; then CANDIDATES+=( "$PROJECT_ROOT/.github/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.claude/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.agents/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.opencode/skills/book-to-skill/scripts/extract.py" ) fi ``` Tests: the nested-directory case is now asserted rather than worked around. `test_opencode_extractor_probe_discovers_supported_layouts` runs every layout from `project/src/nested`, and a new `test_opencode_project_probe_resolves_project_root_from_nested_cwd` covers `.opencode`, `.agents` and `.claude` from `project/src/nested/deeper`. The previous version ran the project layout from the project root, which hid the bug instead of catching it. Also fixes two test-infrastructure faults that made these tests skip on Windows and therefore unable to catch anything: * Bash detection invoked a bare `bash`, which CreateProcess resolves against the system directory before PATH, always picking the WSL stub (`C:\Windows\System32\bash.exe`) even though a working Git bash exists. Now tries absolute candidates — `$OPENCODE_GIT_BASH_PATH`, Git's `../bin/bash.exe`, `/usr/bin/bash` — and validates with `bash -c true`. * Paths returned by Git bash are in MSYS form (`/tmp/...` for `$HOME`) and never compared equal to `pathlib` paths. `_resolve()` now normalises through `cygpath -w` on Windows. Negative control: reverting the SKILL.md change fails exactly the four nested/project cases (4 failed, 12 passed); restoring it returns 16 passed. * fix(hosts): resolve review findings on the rebased selection policy - SKILL.md: remove the duplicated pre-#125 rule-6 orphan left by the conflict resolution; add OpenCode to the host prompt (rule 6) so the unknown-host question names every supported host; repair the frontmatter delimiter that the rebase glued to the description line. - tests/test_scope_selection_contract.py: stop freezing the selection list at exactly six rules and the pre-#209 prompt sentence; assert one contiguous 1..N list plus the required policy instead (review request, 2026-09-22). - tests/test_opencode_host_support.py: same de-freezing for the unknown-host prompt assertion (#209 legitimately grew that list). - tools/validate_skill.py: fix the lens table so the OpenClaw entry keeps its own keys and OpenCode no longer carries a duplicated enforces_allowed_tools/name_pattern pair. Windows-local gates: 695 passed, 16 skipped; the 17 test_hermes failures are the documented WSL-stub bash artifact (Linux CI green). ruff clean; validate_skill passes for claude and opencode lenses. --------- Co-authored-by: wgm66 <wgm66@users.noreply.github.com> | 19 天前 | |
test(security): skip symlink assertion when unsupported (#164) | 1 个月前 | |
fix(extractor): count chapters within part headings (#279) | 7 天前 | |
fix(extractor): translate post-extraction stat errors (#228) * fix(extractor): translate post-extraction stat errors * test: use one utils import style | 30 天前 | |
fix(pdf): stop edge-of-page cleanup deleting real one-word lines (#119) The Roman branch of the bare-page-number pattern listed the letters a numeral may contain (`[ivxlcdm]{1,7}`, case-insensitive) rather than the shape a numeral has. That matches any short word built from those letters, so "MIX", "CIVIL", "DIM", "MILD", "VIVID", "LIVID", "DID" and "ILL" were deleted with no warning whenever they were a page's first or last non-blank line — which is exactly what a part title or a display heading looks like in pdftotext -layout output. Spell the canonical numeral out instead: (?:xc|xl|l?x{0,3})(?:ix|iv|v?i{0,3}) matches every value in 1-99 and nothing else, so no arithmetic or round-trip check is needed. All eight words above are now kept and every real numeral is still stripped. Two deliberate range effects: a lone "c"/"d"/"m" line is now kept, since those only appear in values above the 1-99 front-matter range; and the leading (?=[ivxl]) is the non-empty guard, because both numeral groups are individually optional. | 2 个月前 | |
fix(pdf): strip running headers only at page edges (#113) Two silent deletions survived #119. Boilerplate candidates are collected from the first and last non-blank line of each page, but every occurrence was then removed from every line — so a book whose running header repeats the section title lost the genuine mid-page heading along with the headers. And a page holding a single line cast two votes toward the "repeated on more than half the pages" threshold, since that line is both nb[0] and nb[-1]: a part divider appearing twice in four pages reached 4 votes and was stripped. Removal is now confined to the page edges, which is the only place the candidates were ever detected. Since #101 the cleanup runs on the pypdf and pdfminer paths too, so this covers every PDF extractor. | 2 个月前 | |
fix(config): give each run its own workdir so concurrent extractions cannot clobber each other (#184) Every extraction defaulted to one fixed path, $TMPDIR/book_skill_work, so two runs in flight wrote full_text.txt and metadata.json over each other. Nothing errored. The run that finished second simply replaced the first one's output, and an agent waiting on metadata.json could pick up a different document's extraction and build a skill from the wrong source. The default is now $TMPDIR/book_skill_work-<pid>, so concurrent runs never share a directory. BOOK_SKILL_WORKDIR still overrides it completely. The per-run name is deliberately a sibling of the old fixed path rather than a child of it: an older cleanup routine that removes "book_skill_work" then finds nothing, instead of deleting a live concurrent run's directory. Also fixes a latent case next to it. BOOK_SKILL_WORKDIR set to an empty string resolved to Path(""), i.e. the current directory, which prepare_output_dir() would then populate and chmod to 0700. It now falls back to the default. metadata.json gains a "workdir" field and the completion banner prints the directory, so a consumer can clean up exactly what the run created rather than reconstructing a path. SKILL.md's cleanup step used the retired fixed path and would have silently stopped removing anything; it now removes the reported directory, and the remaining references to the old path are updated. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 1 个月前 | |
feat(skill): optional GitHub publishing for generated skills (#102) Step 10 told people to share a generated skill with `gh skill publish`, which only reaches the Copilot ecosystem. Step 11 adds an opt-in path to a GitHub repo any Agent Skills host can install from, and nothing about it is automatic: the offer appears once, after the Step 9.5 scan passes, and a decline ends it. Visibility is a closed question of its own, asked separately from the offer, and the gate opens only when the answer is the word `public`. An earlier draft searched the reply for the substring, which "public domain", "publicly available" and the Step 10 trigger word "publish" would all have satisfied — in a project whose own benchmark books are public-domain titles. The copyright gate stacks on top: skills derived from third-party books stay private, and access to internal material is not permission to disclose it. Publishing from inside an existing repository is done from a scratch copy, because `git init` in place would record the skill as a gitlink with no `.gitmodules` and fresh clones of the outer project would silently omit it. The report says so, and says how to update later, rather than leaving that to be discovered at the next fold-in. The report's discoverability line is filled from the destination actually used, including the case where it must say a host will NOT see the skill. | 2 个月前 | |
chore: untrack stale compiled bytecode (#94) * chore: untrack stale compiled bytecode scripts/__pycache__/extract.cpython-313.pyc has been tracked since d88e69e ("feat: add EPUB support"), despite .gitignore listing both *.pyc and __pycache__/ — ignore rules do not untrack files already committed. The file was inert: it uses a timestamp-based header recording an 11,321-byte source, while scripts/extract.py is now 871 bytes, so Python always invalidated it and recompiled from source. It also matches no committed revision of extract.py — it was compiled from an uncommitted mid-edit working state and swept in with the surrounding commit. Harmless in practice, but committed bytecode is not reviewable in a diff, which makes it a poor thing to ship from a tool that generates agent skills. Adds tests/test_repo_hygiene.py, which fails if any .pyc or __pycache__/ path is tracked. Verified by re-staging the file: the test fails and names the offender. The test skips cleanly outside a git checkout (e.g. an installed sdist). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: harden the tracked-bytecode guard against quoted and foreign paths Two ways the first version could pass while bytecode was tracked: `git ls-files` quotes non-ASCII paths by default (core.quotePath), emitting "caf\303\251/stale.pyc" — literal quotes and octal escapes. That string ends in neither ".pyc" nor contains "__pycache__/", so the check silently missed it. Switch to `ls-files -z` and split on NUL, which never quotes. If the tree is vendored inside an unrelated git repository, `git ls-files` answers for that repo and can exit 0 with no output, which read as a pass rather than as "not applicable". Compare `rev-parse --show-toplevel` against the test's own repository root and skip when they differ. Verified by staging both shapes: the guard now fails and names the offender for scripts/__pycache__/extract.cpython-313.pyc and for café/stale.pyc. Previously only the first was caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 个月前 | |
fix(skill): stop and ask before re-converting an existing skill (re-run guard) (#224) * fix(skill): stop and ask before re-converting an existing skill (re-run guard) A Full Conversion of a source whose target skill already exists re-runs the entire extraction before Step 5 derives the slug and notices the collision. Mode 4 (Update/Fold-in) cannot catch a naive re-run: its trigger requires the user to request an update, and Step 0's own check only fires when the input path is a skill dir or the slug was given as an argument. Guard: derive the prospective slug under both Step 5 naming forms (author-concept and by-title) plus any SKILL_NAME, check SKILLS_HOME, and on a match STOP and let the user choose fold-in / verify / force regeneration. Recovery after an interrupted run resumes the existing work directory. Tests: tests/test_rerun_guard.py (3 assertions incl. both naming forms). * fix(skill): gate workdir reuse on a matching manifest and resolve the destination before the existing-skill check * fix(skill): gate re-run reuse on a per-source content fingerprint | 19 天前 | |
fix(rtf): decode hex-escaped text (#242) | 19 天前 | |
fix(rtf): preserve escaped control literals in fallback (#290) * fix(rtf): preserve escaped control literals in fallback * test(rtf): make literal regressions runnable in isolation | 7 天前 | |
fix(sanitize): strip deprecated and interlinear-annotation format controls (#182) | 1 个月前 | |
fix(sanitize): strip Default_Ignorable invisible carriers (#250) The invisible-code-point blocklist stripped U+180E MONGOLIAN VOWEL SEPARATOR but not the Mongolian free variation selectors beside it, nor the Khmer inherent vowels or the Duployan shorthand format controls. All are Default_Ignorable and render as nothing, so a run of them after a base character carried a payload through extraction untouched; the generated-skill scanner imports the same predicate, so it did not flag them either. Add them as one commented group unioned into the existing set, which closes both layers at once. The visible neighbours in those blocks and the Cf code points Unicode subtracts from Default_Ignorable as meaningful stay out of the set, with tests holding them out. | 12 天前 | |
fix(extractor): preserve corpus newlines on Windows (#264) | 12 天前 | |
fix(security): scan nested chapters and report markdown left out of scope (#115) Two coverage problems in the generated-skill scanner. Neither is about the rules -- both are about which files get opened. 1. chapters was globbed non-recursively, so a chapter in a sub-directory was never scanned: chapters/ch01.md scanned chapters/part1/ch02.md NOT scanned <- injection passes silently Chapters are in-contract wherever they nest, and the agent reads them either way. Now collected at any depth. 2. When markdown existed outside the contract scope, the CLI still printed "Generated-skill scan passed: no known injection or authority patterns found" with no hint that files had gone unopened. On a skill with an out-of-scope HOW_TO_USE.md and references/notes.md, 6 files carried a payload and 2 were scanned -- and the output said "passed". The bounded scope is intentional and is left alone: scanning unrelated notes would raise false findings, and the existing scope test still holds. What changes is honesty of reporting. Skipped files are now listed, and the pass line is qualified as "found in the scanned scope." Deliberately NOT a Finding, so the exit code is unchanged for a clean skill -- this is advisory information, not a failure. Directory walking uses os.walk(followlinks=False) rather than Path.rglob, because before Python 3.13 rglob descends into symlinked directories, which would let a planted symlink walk the scanner outside the skill tree. Symlinked FILES are still passed through to _read_skill_files so they are reported as errors rather than skipped silently. A test plants a symlinked directory inside chapters/ and asserts the outside file is not scanned. Also relevant to #67, which proposes generating a HOW_TO_USE.md: as things stand that file would land outside the scanned scope, and this at least makes that visible. | 2 个月前 | |
fix(scan): match tool-call control tokens, not the words "tool call" (#152) The prompt.tool_call_tag rule matched \btool[_ -]?call\b anywhere, so every book that explains what a tool call is tripped the gate. A real conversion stopped on three lines that were a glossary entry, a chapter definition and a cheatsheet cell — all ordinary agent vocabulary, no tags, no commands. The scan is a gate: it halts installation and asks for human review, which is the right design and worked. But books about agents and prompting are the most converted kind here, and a gate that always fires on a whole category teaches people to wave it through — which costs more than the false positive, because the next alert that matters gets waved through with it. The rule now requires the delimiters that make it a token rather than a phrase, one alternative per family seen in real chat templates and tool-calling protocols: <tool_call> and </tool_call>, <|tool_call|>, [TOOL_CALL], {{tool_call}}, and "tool_call" as a JSON key or value. The families are listed in a comment so the next person to widen this has the list rather than one report. Updates the existing control-token test, which used a bare "tool_call" line — that premise is the bug — to the delimited form it meant. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
feat(hosts): add Opencode skill discovery and validation (#216) * feat(hosts): add Opencode skill discovery and validation Adds OpenCode (sst/opencode) as a supported skill host, following the same shape as the Hermes Agent host: a validator lens, discovery paths in the converter spec, and tests for both. Validation (`tools/validate_skill.py --lens opencode`): - name must match the official pattern `^[a-z0-9]+(-[a-z0-9]+)*$` (lowercase alphanumeric with single hyphen separators) — stricter than the general Agent Skills default, so dots and underscores are rejected - only the five documented frontmatter keys are recognized: name, description, license, compatibility, metadata - no `allowed-tools` enforcement: OpenCode grants access through `permission.skill` in opencode.json, not frontmatter - no description soft limit; only the 1024-char hard cap applies Discovery (SKILL.md): - personal roots: ~/.config/opencode/skills (OpenCode-managed), plus the ~/.agents/skills and ~/.claude/skills compatibility roots - project roots: .opencode/skills, .agents/skills, .claude/skills - no trust gate, unlike Hermes: OpenCode discovers skills by walking up from the working directory to the git worktree Tests: - 11 `test_opencode_lens_*` cases in tests/test_validate_skill.py - tests/test_opencode_host_support.py, symmetric to the Hermes suite minus its trust-gate cases (OpenCode has no such model); guarded by a real bash probe so it skips instead of failing on hosts where the WSL stub makes `bash` unusable Docs: install.md, architecture.md, how-it-works.md, index.md and the three READMEs list OpenCode alongside the existing hosts. The README repository map also listed only three lenses; it now lists all five. Refs: https://opencode.ai/docs/skills * test(opencode): probe project layout from the project root The `project-opencode` case failed on CI (Linux) while passing locally, because the new host suite is skipped on hosts without a working bash — so the guard hid a real bug until CI ran it. The spec's extractor probe lists project-local roots other than Hermes' as CWD-relative paths (`.opencode/skills/...`, alongside `.github/`, `.claude/`, `.agents/`); only the Hermes block rewrites candidates against `$PROJECT_ROOT`. The test ran the project layout from `project/src/nested`, where no `.opencode/` exists, so `SCRIPT_PATH` stayed empty and the assertion compared the CWD against the extractor. Run project layouts from the project root and keep the personal layouts at a nested CWD, which is what actually demonstrates that personal roots resolve as absolute `$HOME/...` paths independent of depth. Verified under a real bash: the project case now resolves, and the three personal cases (`.config/opencode`, `.agents`, `.claude`) still resolve from `project/src/nested`. * fix(hosts): resolve project-local skills from the git worktree Project-local candidates were listed as bare CWD-relative paths (`.opencode/skills/...`, `.github/...`, `.claude/...`, `.agents/...`), so the converter was only found when the agent happened to sit at the project root. From a nested directory such as `project/src/nested` the probe returned nothing and Step 2 failed with "Could not find scripts/extract.py". That contradicts how the hosts behave: OpenCode walks up from the working directory to the git worktree, so project skills are meant to be reachable from anywhere inside the project. Prepend the same four roots resolved against `$PROJECT_ROOT` (already computed by the probe) when it is non-empty, keeping the CWD-relative forms afterwards so behaviour outside a git repository is unchanged: ```bash if [ -n "$PROJECT_ROOT" ]; then CANDIDATES+=( "$PROJECT_ROOT/.github/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.claude/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.agents/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.opencode/skills/book-to-skill/scripts/extract.py" ) fi ``` Tests: the nested-directory case is now asserted rather than worked around. `test_opencode_extractor_probe_discovers_supported_layouts` runs every layout from `project/src/nested`, and a new `test_opencode_project_probe_resolves_project_root_from_nested_cwd` covers `.opencode`, `.agents` and `.claude` from `project/src/nested/deeper`. The previous version ran the project layout from the project root, which hid the bug instead of catching it. Also fixes two test-infrastructure faults that made these tests skip on Windows and therefore unable to catch anything: * Bash detection invoked a bare `bash`, which CreateProcess resolves against the system directory before PATH, always picking the WSL stub (`C:\Windows\System32\bash.exe`) even though a working Git bash exists. Now tries absolute candidates — `$OPENCODE_GIT_BASH_PATH`, Git's `../bin/bash.exe`, `/usr/bin/bash` — and validates with `bash -c true`. * Paths returned by Git bash are in MSYS form (`/tmp/...` for `$HOME`) and never compared equal to `pathlib` paths. `_resolve()` now normalises through `cygpath -w` on Windows. Negative control: reverting the SKILL.md change fails exactly the four nested/project cases (4 failed, 12 passed); restoring it returns 16 passed. * fix(hosts): resolve review findings on the rebased selection policy - SKILL.md: remove the duplicated pre-#125 rule-6 orphan left by the conflict resolution; add OpenCode to the host prompt (rule 6) so the unknown-host question names every supported host; repair the frontmatter delimiter that the rebase glued to the description line. - tests/test_scope_selection_contract.py: stop freezing the selection list at exactly six rules and the pre-#209 prompt sentence; assert one contiguous 1..N list plus the required policy instead (review request, 2026-09-22). - tests/test_opencode_host_support.py: same de-freezing for the unknown-host prompt assertion (#209 legitimately grew that list). - tools/validate_skill.py: fix the lens table so the OpenClaw entry keeps its own keys and OpenCode no longer carries a duplicated enforces_allowed_tools/name_pattern pair. Windows-local gates: 695 passed, 16 skipped; the 17 test_hermes failures are the documented WSL-stub bash artifact (Linux CI green). ruff clean; validate_skill passes for claude and opencode lenses. --------- Co-authored-by: wgm66 <wgm66@users.noreply.github.com> | 19 天前 | |
fix(extractor): reject punctuation-only setext titles, matching the ATX branch (#180) _structural_chapter_count has two heading branches. The ATX branch rejects a title with no word character -- that is what stops a "=====" table border or a "***" thematic break being counted: if title and re.search(r"\w", title): The setext branch had no equivalent, so the identical string was rejected as "## ***" and accepted as "***" sitting above a row of "-". Every one of these minted a phantom heading (two real sections in each fixture): title setext before setext after ATX *** 3 2 2 +-----+ 3 2 2 ....... 3 2 2 ||||| 3 2 2 * * * 3 2 2 ### 3 2 2 -- -- 3 2 2 Two thematic breaks in a row ("***" over "---") are both valid Markdown thematic breaks and produced a heading titled "***". The function's own docstring already claims "thematic breaks, table borders, and front-matter '---' do not match". That was only true while the underline was shorter than the line above it -- a long rule defeated the length guard. Surfaced by the reviewer's analysis on #135, which traced an over-count to this branch having no guard. That PR accepted the cost for unterminated fences; this removes the part of it that is unambiguous, and only that part. Deliberately unchanged: a title with word characters ("Options" above "-------", a plain-text table header) is genuinely ambiguous and still counts, and "___" is still counted by BOTH branches because "_" is a word character to \w. The latter is pinned by a parity test so closing it later is a deliberate change to both branches rather than a silent divergence. | 1 个月前 | |
fix(extractor): exclude framing headings from structural chapters (#253) | 12 天前 | |
fix(extractor): exclude framing headings from structural chapters (#253) | 12 天前 | |
fix(extractor): expand `~` in input paths (#118) A glob must be quoted to reach the extractor unexpanded ("~/books/*.epub"), but quoting also stops the shell expanding the tilde. `glob.glob` and `Path` both treat "~" as a literal directory name, so the README's own quoted-glob example matched nothing and the run exited with "No supported files found"; a quoted "~/book.pdf" became "<cwd>/~/book.pdf" and reported a confusing File not found. Normalise with os.path.expanduser once at the top of the resolve loop, so the glob branch and the file/directory branch both see a real path. expanduser is identity for paths without a leading "~" and returns an unresolvable "~user" unchanged, so it is safe unconditionally. | 2 个月前 | |
fix(scan): match tool-call control tokens, not the words "tool call" (#152) The prompt.tool_call_tag rule matched \btool[_ -]?call\b anywhere, so every book that explains what a tool call is tripped the gate. A real conversion stopped on three lines that were a glossary entry, a chapter definition and a cheatsheet cell — all ordinary agent vocabulary, no tags, no commands. The scan is a gate: it halts installation and asks for human review, which is the right design and worked. But books about agents and prompting are the most converted kind here, and a gate that always fires on a whole category teaches people to wave it through — which costs more than the false positive, because the next alert that matters gets waved through with it. The rule now requires the delimiters that make it a token rather than a phrase, one alternative per family seen in real chat templates and tool-calling protocols: <tool_call> and </tool_call>, <|tool_call|>, [TOOL_CALL], {{tool_call}}, and "tool_call" as a JSON key or value. The families are listed in a comment so the next person to widen this has the list rather than one report. Updates the existing control-token test, which used a bare "tool_call" line — that premise is the bug — to the delimited form it meant. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
docs: localize Russian headings and guard translation policy (#281) | 7 天前 | |
fix(extractor): stop an unbalanced code fence swallowing later headings (#135) _structural_chapter_count tracked fenced blocks by flipping a boolean on every fence line, so a fence that never closed left the scanner 'inside a code block' for the rest of the file and every heading after it was dropped. On an 8-section handbook with one missing closer, 5 of 8 sections disappeared — silently, with extraction reporting success. The blast radius was aimed at the wrong people: this fallback only runs when numeric 'Chapter N' detection finds nothing, i.e. on Markdown and AsciiDoc technical docs, exactly the sources full of fenced blocks. Fences are now resolved in a pre-pass that records only closed blocks, and a closer must match its opener's character, per CommonMark. Unterminated input degrades instead of truncating — the same trade taken for RTF destination groups in #109. The cost is pinned by test: an unclosed fence holding CLI output over-counts by one, because the digit-led and all-punctuation guards live on the ATX branch and setext has no equivalent. Losing one spurious heading beats losing half a book's structure. | 2 个月前 | |
feat(hosts): add Opencode skill discovery and validation (#216) * feat(hosts): add Opencode skill discovery and validation Adds OpenCode (sst/opencode) as a supported skill host, following the same shape as the Hermes Agent host: a validator lens, discovery paths in the converter spec, and tests for both. Validation (`tools/validate_skill.py --lens opencode`): - name must match the official pattern `^[a-z0-9]+(-[a-z0-9]+)*$` (lowercase alphanumeric with single hyphen separators) — stricter than the general Agent Skills default, so dots and underscores are rejected - only the five documented frontmatter keys are recognized: name, description, license, compatibility, metadata - no `allowed-tools` enforcement: OpenCode grants access through `permission.skill` in opencode.json, not frontmatter - no description soft limit; only the 1024-char hard cap applies Discovery (SKILL.md): - personal roots: ~/.config/opencode/skills (OpenCode-managed), plus the ~/.agents/skills and ~/.claude/skills compatibility roots - project roots: .opencode/skills, .agents/skills, .claude/skills - no trust gate, unlike Hermes: OpenCode discovers skills by walking up from the working directory to the git worktree Tests: - 11 `test_opencode_lens_*` cases in tests/test_validate_skill.py - tests/test_opencode_host_support.py, symmetric to the Hermes suite minus its trust-gate cases (OpenCode has no such model); guarded by a real bash probe so it skips instead of failing on hosts where the WSL stub makes `bash` unusable Docs: install.md, architecture.md, how-it-works.md, index.md and the three READMEs list OpenCode alongside the existing hosts. The README repository map also listed only three lenses; it now lists all five. Refs: https://opencode.ai/docs/skills * test(opencode): probe project layout from the project root The `project-opencode` case failed on CI (Linux) while passing locally, because the new host suite is skipped on hosts without a working bash — so the guard hid a real bug until CI ran it. The spec's extractor probe lists project-local roots other than Hermes' as CWD-relative paths (`.opencode/skills/...`, alongside `.github/`, `.claude/`, `.agents/`); only the Hermes block rewrites candidates against `$PROJECT_ROOT`. The test ran the project layout from `project/src/nested`, where no `.opencode/` exists, so `SCRIPT_PATH` stayed empty and the assertion compared the CWD against the extractor. Run project layouts from the project root and keep the personal layouts at a nested CWD, which is what actually demonstrates that personal roots resolve as absolute `$HOME/...` paths independent of depth. Verified under a real bash: the project case now resolves, and the three personal cases (`.config/opencode`, `.agents`, `.claude`) still resolve from `project/src/nested`. * fix(hosts): resolve project-local skills from the git worktree Project-local candidates were listed as bare CWD-relative paths (`.opencode/skills/...`, `.github/...`, `.claude/...`, `.agents/...`), so the converter was only found when the agent happened to sit at the project root. From a nested directory such as `project/src/nested` the probe returned nothing and Step 2 failed with "Could not find scripts/extract.py". That contradicts how the hosts behave: OpenCode walks up from the working directory to the git worktree, so project skills are meant to be reachable from anywhere inside the project. Prepend the same four roots resolved against `$PROJECT_ROOT` (already computed by the probe) when it is non-empty, keeping the CWD-relative forms afterwards so behaviour outside a git repository is unchanged: ```bash if [ -n "$PROJECT_ROOT" ]; then CANDIDATES+=( "$PROJECT_ROOT/.github/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.claude/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.agents/skills/book-to-skill/scripts/extract.py" "$PROJECT_ROOT/.opencode/skills/book-to-skill/scripts/extract.py" ) fi ``` Tests: the nested-directory case is now asserted rather than worked around. `test_opencode_extractor_probe_discovers_supported_layouts` runs every layout from `project/src/nested`, and a new `test_opencode_project_probe_resolves_project_root_from_nested_cwd` covers `.opencode`, `.agents` and `.claude` from `project/src/nested/deeper`. The previous version ran the project layout from the project root, which hid the bug instead of catching it. Also fixes two test-infrastructure faults that made these tests skip on Windows and therefore unable to catch anything: * Bash detection invoked a bare `bash`, which CreateProcess resolves against the system directory before PATH, always picking the WSL stub (`C:\Windows\System32\bash.exe`) even though a working Git bash exists. Now tries absolute candidates — `$OPENCODE_GIT_BASH_PATH`, Git's `../bin/bash.exe`, `/usr/bin/bash` — and validates with `bash -c true`. * Paths returned by Git bash are in MSYS form (`/tmp/...` for `$HOME`) and never compared equal to `pathlib` paths. `_resolve()` now normalises through `cygpath -w` on Windows. Negative control: reverting the SKILL.md change fails exactly the four nested/project cases (4 failed, 12 passed); restoring it returns 16 passed. * fix(hosts): resolve review findings on the rebased selection policy - SKILL.md: remove the duplicated pre-#125 rule-6 orphan left by the conflict resolution; add OpenCode to the host prompt (rule 6) so the unknown-host question names every supported host; repair the frontmatter delimiter that the rebase glued to the description line. - tests/test_scope_selection_contract.py: stop freezing the selection list at exactly six rules and the pre-#209 prompt sentence; assert one contiguous 1..N list plus the required policy instead (review request, 2026-09-22). - tests/test_opencode_host_support.py: same de-freezing for the unknown-host prompt assertion (#209 legitimately grew that list). - tools/validate_skill.py: fix the lens table so the OpenClaw entry keeps its own keys and OpenCode no longer carries a duplicated enforces_allowed_tools/name_pattern pair. Windows-local gates: 695 passed, 16 skipped; the 17 test_hermes failures are the documented WSL-stub bash artifact (Linux CI green). ruff clean; validate_skill passes for claude and opencode lenses. --------- Co-authored-by: wgm66 <wgm66@users.noreply.github.com> | 19 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 7 天前 | ||
| 30 天前 | ||
| 7 天前 | ||
| 19 天前 | ||
| 2 个月前 | ||
| 12 天前 | ||
| 6 天前 | ||
| 2 个月前 | ||
| 7 天前 | ||
| 12 天前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 7 天前 | ||
| 7 天前 | ||
| 1 个月前 | ||
| 7 天前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 7 天前 | ||
| 2 个月前 | ||
| 7 天前 | ||
| 2 个月前 | ||
| 25 天前 | ||
| 2 个月前 | ||
| 30 天前 | ||
| 19 天前 | ||
| 1 个月前 | ||
| 7 天前 | ||
| 30 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 19 天前 | ||
| 19 天前 | ||
| 7 天前 | ||
| 1 个月前 | ||
| 12 天前 | ||
| 12 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 19 天前 | ||
| 1 个月前 | ||
| 12 天前 | ||
| 12 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 7 天前 | ||
| 2 个月前 | ||
| 19 天前 |