paperlint 2.0.0
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/.github/dependabot.yml +72 -0
- package/.github/workflows/ci.yml +297 -0
- package/.github/workflows/dependabot-automerge.yml +70 -0
- package/.github/workflows/pr-title.yml +59 -0
- package/.github/workflows/release.yml +54 -0
- package/CLAUDE.md +598 -0
- package/CONTRIBUTING.md +159 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/action.harness.mjs +287 -0
- package/action.mutations.mjs +162 -0
- package/action.yml +138 -0
- package/bin/rpp.mjs +43 -0
- package/dist/action-ref.d.ts +12 -0
- package/dist/action-ref.d.ts.map +1 -0
- package/dist/action-ref.js +16 -0
- package/dist/action-ref.js.map +1 -0
- package/dist/adapters/banal/failure.d.ts +73 -0
- package/dist/adapters/banal/failure.d.ts.map +1 -0
- package/dist/adapters/banal/failure.js +58 -0
- package/dist/adapters/banal/failure.js.map +1 -0
- package/dist/adapters/banal/index.d.ts +17 -0
- package/dist/adapters/banal/index.d.ts.map +1 -0
- package/dist/adapters/banal/index.js +56 -0
- package/dist/adapters/banal/index.js.map +1 -0
- package/dist/adapters/banal/install.d.ts +26 -0
- package/dist/adapters/banal/install.d.ts.map +1 -0
- package/dist/adapters/banal/install.js +15 -0
- package/dist/adapters/banal/install.js.map +1 -0
- package/dist/adapters/banal/invocation.d.ts +48 -0
- package/dist/adapters/banal/invocation.d.ts.map +1 -0
- package/dist/adapters/banal/invocation.js +43 -0
- package/dist/adapters/banal/invocation.js.map +1 -0
- package/dist/adapters/banal/locate.d.ts +50 -0
- package/dist/adapters/banal/locate.d.ts.map +1 -0
- package/dist/adapters/banal/locate.js +34 -0
- package/dist/adapters/banal/locate.js.map +1 -0
- package/dist/adapters/banal/output.d.ts +27 -0
- package/dist/adapters/banal/output.d.ts.map +1 -0
- package/dist/adapters/banal/output.js +112 -0
- package/dist/adapters/banal/output.js.map +1 -0
- package/dist/adapters/banal/pin.d.ts +19 -0
- package/dist/adapters/banal/pin.d.ts.map +1 -0
- package/dist/adapters/banal/pin.js +15 -0
- package/dist/adapters/banal/pin.js.map +1 -0
- package/dist/adapters/banal/probe.d.ts +12 -0
- package/dist/adapters/banal/probe.d.ts.map +1 -0
- package/dist/adapters/banal/probe.js +27 -0
- package/dist/adapters/banal/probe.js.map +1 -0
- package/dist/adapters/banal/run.d.ts +89 -0
- package/dist/adapters/banal/run.d.ts.map +1 -0
- package/dist/adapters/banal/run.js +104 -0
- package/dist/adapters/banal/run.js.map +1 -0
- package/dist/adapters/banal/settings.d.ts +18 -0
- package/dist/adapters/banal/settings.d.ts.map +1 -0
- package/dist/adapters/banal/settings.js +29 -0
- package/dist/adapters/banal/settings.js.map +1 -0
- package/dist/adapters/banal/xml.d.ts +48 -0
- package/dist/adapters/banal/xml.d.ts.map +1 -0
- package/dist/adapters/banal/xml.js +67 -0
- package/dist/adapters/banal/xml.js.map +1 -0
- package/dist/adapters/curl/download.io.d.ts +14 -0
- package/dist/adapters/curl/download.io.d.ts.map +1 -0
- package/dist/adapters/curl/download.io.js +69 -0
- package/dist/adapters/curl/download.io.js.map +1 -0
- package/dist/adapters/curl/index.d.ts +6 -0
- package/dist/adapters/curl/index.d.ts.map +1 -0
- package/dist/adapters/curl/index.js +6 -0
- package/dist/adapters/curl/index.js.map +1 -0
- package/dist/adapters/memory/index.d.ts +43 -0
- package/dist/adapters/memory/index.d.ts.map +1 -0
- package/dist/adapters/memory/index.js +79 -0
- package/dist/adapters/memory/index.js.map +1 -0
- package/dist/adapters/node/files.io.d.ts +3 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -0
- package/dist/adapters/node/files.io.js +31 -0
- package/dist/adapters/node/files.io.js.map +1 -0
- package/dist/adapters/node/host.io.d.ts +3 -0
- package/dist/adapters/node/host.io.d.ts.map +1 -0
- package/dist/adapters/node/host.io.js +14 -0
- package/dist/adapters/node/host.io.js.map +1 -0
- package/dist/adapters/node/index.d.ts +25 -0
- package/dist/adapters/node/index.d.ts.map +1 -0
- package/dist/adapters/node/index.js +14 -0
- package/dist/adapters/node/index.js.map +1 -0
- package/dist/adapters/node/process.io.d.ts +14 -0
- package/dist/adapters/node/process.io.d.ts.map +1 -0
- package/dist/adapters/node/process.io.js +41 -0
- package/dist/adapters/node/process.io.js.map +1 -0
- package/dist/adapters/node/workspace.io.d.ts +4 -0
- package/dist/adapters/node/workspace.io.d.ts.map +1 -0
- package/dist/adapters/node/workspace.io.js +33 -0
- package/dist/adapters/node/workspace.io.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts +42 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
- package/dist/adapters/pdfjs/fill.js +91 -0
- package/dist/adapters/pdfjs/fill.js.map +1 -0
- package/dist/build-engine.d.ts +48 -0
- package/dist/build-engine.d.ts.map +1 -0
- package/dist/build-engine.js +148 -0
- package/dist/build-engine.js.map +1 -0
- package/dist/build.d.ts +163 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +575 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +151 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +951 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +280 -0
- package/dist/doctor.js.map +1 -0
- package/dist/domain/geometry.d.ts +71 -0
- package/dist/domain/geometry.d.ts.map +1 -0
- package/dist/domain/geometry.js +35 -0
- package/dist/domain/geometry.js.map +1 -0
- package/dist/domain/host.d.ts +16 -0
- package/dist/domain/host.d.ts.map +1 -0
- package/dist/domain/host.js +8 -0
- package/dist/domain/host.js.map +1 -0
- package/dist/domain/page-layout.d.ts +34 -0
- package/dist/domain/page-layout.d.ts.map +1 -0
- package/dist/domain/page-layout.js +8 -0
- package/dist/domain/page-layout.js.map +1 -0
- package/dist/domain/paths.d.ts +5 -0
- package/dist/domain/paths.d.ts.map +1 -0
- package/dist/domain/paths.js +2 -0
- package/dist/domain/paths.js.map +1 -0
- package/dist/domain/result.d.ts +23 -0
- package/dist/domain/result.d.ts.map +1 -0
- package/dist/domain/result.js +10 -0
- package/dist/domain/result.js.map +1 -0
- package/dist/domain/sha256.d.ts +7 -0
- package/dist/domain/sha256.d.ts.map +1 -0
- package/dist/domain/sha256.js +14 -0
- package/dist/domain/sha256.js.map +1 -0
- package/dist/domain/text.d.ts +6 -0
- package/dist/domain/text.d.ts.map +1 -0
- package/dist/domain/text.js +7 -0
- package/dist/domain/text.js.map +1 -0
- package/dist/engine.d.ts +93 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +119 -0
- package/dist/engine.js.map +1 -0
- package/dist/exit-code.d.ts +22 -0
- package/dist/exit-code.d.ts.map +1 -0
- package/dist/exit-code.js +10 -0
- package/dist/exit-code.js.map +1 -0
- package/dist/facts-file.d.ts +96 -0
- package/dist/facts-file.d.ts.map +1 -0
- package/dist/facts-file.js +134 -0
- package/dist/facts-file.js.map +1 -0
- package/dist/hooks-settings.d.ts +141 -0
- package/dist/hooks-settings.d.ts.map +1 -0
- package/dist/hooks-settings.js +306 -0
- package/dist/hooks-settings.js.map +1 -0
- package/dist/init.d.ts +201 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +579 -0
- package/dist/init.js.map +1 -0
- package/dist/latex-log.d.ts +80 -0
- package/dist/latex-log.d.ts.map +1 -0
- package/dist/latex-log.js +187 -0
- package/dist/latex-log.js.map +1 -0
- package/dist/latex-loop.d.ts +129 -0
- package/dist/latex-loop.d.ts.map +1 -0
- package/dist/latex-loop.js +113 -0
- package/dist/latex-loop.js.map +1 -0
- package/dist/link-skills.d.ts +51 -0
- package/dist/link-skills.d.ts.map +1 -0
- package/dist/link-skills.js +199 -0
- package/dist/link-skills.js.map +1 -0
- package/dist/new-paper.d.ts +48 -0
- package/dist/new-paper.d.ts.map +1 -0
- package/dist/new-paper.js +110 -0
- package/dist/new-paper.js.map +1 -0
- package/dist/pdf-facts.d.ts +44 -0
- package/dist/pdf-facts.d.ts.map +1 -0
- package/dist/pdf-facts.js +239 -0
- package/dist/pdf-facts.js.map +1 -0
- package/dist/pdf-geometry.d.ts +170 -0
- package/dist/pdf-geometry.d.ts.map +1 -0
- package/dist/pdf-geometry.js +158 -0
- package/dist/pdf-geometry.js.map +1 -0
- package/dist/ports/download.d.ts +9 -0
- package/dist/ports/download.d.ts.map +1 -0
- package/dist/ports/download.js +2 -0
- package/dist/ports/download.js.map +1 -0
- package/dist/ports/files.d.ts +11 -0
- package/dist/ports/files.d.ts.map +1 -0
- package/dist/ports/files.js +2 -0
- package/dist/ports/files.js.map +1 -0
- package/dist/ports/measure-geometry.d.ts +8 -0
- package/dist/ports/measure-geometry.d.ts.map +1 -0
- package/dist/ports/measure-geometry.js +2 -0
- package/dist/ports/measure-geometry.js.map +1 -0
- package/dist/ports/process.d.ts +45 -0
- package/dist/ports/process.d.ts.map +1 -0
- package/dist/ports/process.js +2 -0
- package/dist/ports/process.js.map +1 -0
- package/dist/ports/tool-installer.d.ts +29 -0
- package/dist/ports/tool-installer.d.ts.map +1 -0
- package/dist/ports/tool-installer.js +2 -0
- package/dist/ports/tool-installer.js.map +1 -0
- package/dist/ports/workspace.d.ts +18 -0
- package/dist/ports/workspace.d.ts.map +1 -0
- package/dist/ports/workspace.js +2 -0
- package/dist/ports/workspace.js.map +1 -0
- package/dist/rules-config.d.ts +34 -0
- package/dist/rules-config.d.ts.map +1 -0
- package/dist/rules-config.js +132 -0
- package/dist/rules-config.js.map +1 -0
- package/dist/structure.d.ts +34 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +149 -0
- package/dist/structure.js.map +1 -0
- package/dist/tex-requirements.d.ts +43 -0
- package/dist/tex-requirements.d.ts.map +1 -0
- package/dist/tex-requirements.js +127 -0
- package/dist/tex-requirements.js.map +1 -0
- package/dist/toolchain.d.ts +159 -0
- package/dist/toolchain.d.ts.map +1 -0
- package/dist/toolchain.js +542 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/configuration.md +235 -0
- package/docs/e2e.md +152 -0
- package/docs/incidents.md +59 -0
- package/docs/install.md +170 -0
- package/docs/optional-rules.md +107 -0
- package/docs/package-shape-options.md +262 -0
- package/docs/prior-art/README.md +76 -0
- package/docs/prior-art/blocking-vs-advisory.md +83 -0
- package/docs/prior-art/content-delivery.md +124 -0
- package/docs/prior-art/multi-mode-tools.md +106 -0
- package/docs/prior-art/nondeterministic-checks.md +99 -0
- package/docs/prior-art/package-location.md +422 -0
- package/docs/prior-art/paper-folder-scaffolding.md +538 -0
- package/docs/prior-art/readme-structure.md +69 -0
- package/docs/prior-art/repro/README.md +92 -0
- package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
- package/docs/prior-art/repro/claim1-at2.mjs +40 -0
- package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
- package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
- package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
- package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
- package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
- package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
- package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
- package/docs/prior-art/repro/claim3-imports.mjs +50 -0
- package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
- package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
- package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
- package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
- package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
- package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
- package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
- package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
- package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
- package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
- package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
- package/docs/prior-art/test-tooling.md +131 -0
- package/docs/rules.md +58 -0
- package/docs/texlive-install-decision.md +230 -0
- package/docs/toolchain.md +152 -0
- package/eslint-rules/doc-fields.harness.mjs +336 -0
- package/eslint-rules/doc-fields.mjs +186 -0
- package/eslint-rules/doc-fields.mutations.mjs +96 -0
- package/eslint-rules/install-path-literals.harness.mjs +121 -0
- package/eslint-rules/install-path-literals.mjs +108 -0
- package/eslint-rules/install-path-literals.mutations.mjs +62 -0
- package/eslint-rules/latex-language.harness.mjs +599 -0
- package/eslint-rules/latex-language.mjs +591 -0
- package/eslint-rules/latex-language.mutations.mjs +196 -0
- package/eslint-rules/paper-research-question.harness.mjs +146 -0
- package/eslint-rules/paper-research-question.mjs +180 -0
- package/eslint-rules/paper-research-question.mutations.mjs +127 -0
- package/eslint-rules/paper-stages.harness.mjs +356 -0
- package/eslint-rules/paper-stages.mjs +455 -0
- package/eslint-rules/paper-stages.mutations.mjs +157 -0
- package/eslint-rules/paper-typography.harness.mjs +291 -0
- package/eslint-rules/paper-typography.mjs +313 -0
- package/eslint-rules/paper-typography.mutations.mjs +131 -0
- package/eslint-rules/papers.harness.mjs +259 -0
- package/eslint-rules/papers.mjs +166 -0
- package/eslint-rules/papers.mutations.mjs +186 -0
- package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
- package/eslint-rules/pdf-last-page-balance.mjs +208 -0
- package/eslint-rules/review-findings-cause.harness.mjs +228 -0
- package/eslint-rules/review-findings-cause.mjs +135 -0
- package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
- package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
- package/eslint-rules/temp-root-realpath.mjs +129 -0
- package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
- package/eslint-rules/tex-build.harness.mjs +753 -0
- package/eslint-rules/tex-build.mjs +322 -0
- package/eslint-rules/tex-build.mutations.mjs +258 -0
- package/eslint.config.mjs +521 -0
- package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/acmart/paper.tex +11 -0
- package/fixtures/build-e2e/acmart/venue.json +1 -0
- package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/broken/paper.tex +7 -0
- package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/cite/build.sh +5 -0
- package/fixtures/build-e2e/cite/paper.tex +10 -0
- package/fixtures/build-e2e/cite/refs.bib +9 -0
- package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/empty/paper.tex +6 -0
- package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/fallback/paper.tex +11 -0
- package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/guards/paper.tex +10 -0
- package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
- package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
- package/fixtures/install-path-literals/clean.md +15 -0
- package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
- package/fixtures/install-path-literals/defect.md +14 -0
- package/fixtures/latex-language/clean.tex +50 -0
- package/fixtures/latex-language/defect.tex +52 -0
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
- package/fixtures/paper-research-question/draft/paper.tex +2 -0
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
- package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
- package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
- package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
- package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
- package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
- package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
- package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
- package/fixtures/pdf-facts/README.md +22 -0
- package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
- package/fixtures/pdf-facts/encrypted.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.tex +28 -0
- package/fixtures/pdf-facts/t3-all.pdf +0 -0
- package/fixtures/pdf-facts/t3-all.tex +8 -0
- package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
- package/fixtures/pdf-facts/t3-mixed.tex +9 -0
- package/fixtures/pdf-facts/ttf.pdf +2240 -1
- package/fixtures/pdf-facts/ttf.tex +6 -0
- package/fixtures/real-markdown-paper/baseline.json +24 -0
- package/fixtures/real-markdown-paper/baseline.mjs +48 -0
- package/fixtures/render-paper/build-clean.sh +25 -0
- package/fixtures/render-paper/build-defect.sh +15 -0
- package/fixtures/review-findings-cause/clean.md +17 -0
- package/fixtures/review-findings-cause/defect.md +14 -0
- package/fixtures/review-findings-cause/old-debt.md +14 -0
- package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
- package/fixtures/tex-build/clean.tex +21 -0
- package/fixtures/tex-build/defect.tex +24 -0
- package/fixtures/tex-build/frontmatter-clean.tex +25 -0
- package/fixtures/tex-build/frontmatter-defect.tex +23 -0
- package/fixtures/toolchain-mirror/catalog.txt +5 -0
- package/fixtures/toolchain-mirror/install-tl +27 -0
- package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
- package/fixtures/toolchain-mirror/release-year +1 -0
- package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
- package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
- package/hooks/hooks.harness.mjs +713 -0
- package/hooks/hooks.mutations.mjs +337 -0
- package/hooks/paper-edit-guard.hook.d.mts +13 -0
- package/hooks/paper-edit-guard.hook.mjs +457 -0
- package/hooks/paper-skills-nudge.hook.mjs +136 -0
- package/hooks/paper-status-gates.hook.mjs +156 -0
- package/hooks/paper-status-gates.sh +91 -0
- package/lib/agent-cli-version.harness.mjs +165 -0
- package/lib/agent-cli-version.mjs +106 -0
- package/lib/agent-cli-version.mutations.mjs +109 -0
- package/lib/markdown.mjs +386 -0
- package/lib/mutation-driver.harness.mjs +227 -0
- package/lib/mutation-driver.mjs +397 -0
- package/lib/mutation-driver.mutations.mjs +68 -0
- package/lib/paper-config.d.mts +34 -0
- package/lib/paper-config.harness.mjs +286 -0
- package/lib/paper-config.mjs +142 -0
- package/lib/paper-config.mutations.mjs +143 -0
- package/lib/skill-checks.mjs +701 -0
- package/lib/skill-corpus.mjs +403 -0
- package/lib/skill-eval-fixture.mjs +63 -0
- package/lib/skill-eval-kit.mjs +257 -0
- package/lib/skill-trigger-cases.harness.mjs +170 -0
- package/lib/skill-trigger-cases.mjs +446 -0
- package/lib/skill-trigger-cases.mutations.mjs +65 -0
- package/lib/trigger-ledger.mjs +215 -0
- package/package.json +97 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/hooks/hooks.json +30 -0
- package/scripts/check.harness.mjs +177 -0
- package/scripts/check.mjs +239 -0
- package/scripts/check.mutations.mjs +110 -0
- package/scripts/eslint-report-guard.mjs +82 -0
- package/scripts/exclusive.mjs +138 -0
- package/scripts/harness-api.frozen.json +76 -0
- package/scripts/harness-api.test.ts +175 -0
- package/scripts/layer-legacy-frozen.d.mts +28 -0
- package/scripts/layer-legacy-frozen.mjs +152 -0
- package/scripts/layer-legacy-frozen.test.ts +115 -0
- package/scripts/layer-legacy.frozen.json +50 -0
- package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
- package/scripts/mutation-batteries-frozen.mjs +238 -0
- package/scripts/mutation-batteries.frozen.json +117 -0
- package/scripts/release-config.test.ts +90 -0
- package/scripts/rules-are-content-only.harness.mjs +113 -0
- package/scripts/rules-are-content-only.mjs +138 -0
- package/scripts/rules-are-content-only.mutations.mjs +81 -0
- package/scripts/rules-see-files.harness.mjs +115 -0
- package/scripts/rules-see-files.mjs +99 -0
- package/scripts/rules-see-files.mutations.mjs +131 -0
- package/scripts/run-mutations.mjs +100 -0
- package/scripts/semantic-release-plugins.d.ts +16 -0
- package/skills/README.md +15 -0
- package/skills/analyze-sibling-paper/SKILL.md +170 -0
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
- package/skills/argument-arc/SKILL.md +177 -0
- package/skills/argument-arc/SKILL.md.spec.ts +192 -0
- package/skills/argument-arc/argument-arc.eval.mjs +19 -0
- package/skills/argument-arc/argument-arc.harness.mjs +23 -0
- package/skills/build-benchmark/SKILL.md +213 -0
- package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
- package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
- package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
- package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
- package/skills/camera-ready/SKILL.md +148 -0
- package/skills/camera-ready/SKILL.md.spec.ts +164 -0
- package/skills/camera-ready/camera-ready.eval.mjs +19 -0
- package/skills/camera-ready/camera-ready.harness.mjs +23 -0
- package/skills/cold-read-diff/SKILL.md +160 -0
- package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
- package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
- package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
- package/skills/draft-paper/SKILL.md +152 -0
- package/skills/draft-paper/SKILL.md.spec.ts +169 -0
- package/skills/draft-paper/draft-paper.eval.mjs +19 -0
- package/skills/draft-paper/draft-paper.harness.mjs +23 -0
- package/skills/extend-paper/SKILL.md +99 -0
- package/skills/extend-paper/SKILL.md.spec.ts +116 -0
- package/skills/extend-paper/extend-paper.eval.mjs +19 -0
- package/skills/extend-paper/extend-paper.harness.mjs +23 -0
- package/skills/find-venue/SKILL.md +128 -0
- package/skills/find-venue/SKILL.md.spec.ts +145 -0
- package/skills/find-venue/find-venue.eval.mjs +19 -0
- package/skills/find-venue/find-venue.harness.mjs +23 -0
- package/skills/grade-paper-writing/SKILL.md +436 -0
- package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
- package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
- package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
- package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
- package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
- package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
- package/skills/grade-paper-writing/prose-lint.mjs +713 -0
- package/skills/harden-paper/SKILL.md +318 -0
- package/skills/harden-paper/SKILL.md.spec.ts +336 -0
- package/skills/harden-paper/check-numbers.sh +33 -0
- package/skills/harden-paper/check-release-claims.sh +35 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
- package/skills/harden-paper/harden-paper.eval.mjs +19 -0
- package/skills/harden-paper/harden-paper.harness.mjs +23 -0
- package/skills/map-prior-work/SKILL.md +211 -0
- package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
- package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
- package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
- package/skills/osf-artifact-upload/SKILL.md +52 -0
- package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
- package/skills/paper-adversarial-review/SKILL.md +126 -0
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
- package/skills/paper-pipeline/SKILL.md +499 -0
- package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
- package/skills/paper-pipeline/description-language.eval.mjs +347 -0
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
- package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
- package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
- package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
- package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
- package/skills/paper-pipeline/references/anonymization.md +68 -0
- package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
- package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
- package/skills/paper-pipeline/references/credit-criteria.md +69 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
- package/skills/paper-pipeline/references/review-ratchet.md +36 -0
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
- package/skills/paper-pipeline/references/writing-craft.md +448 -0
- package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
- package/skills/paper-pipeline/repro/README.md +129 -0
- package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
- package/skills/paper-pipeline/scripts/README.md +344 -0
- package/skills/paper-pipeline/scripts/announce.mjs +67 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
- package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
- package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
- package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
- package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
- package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
- package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
- package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
- package/skills/paper-pipeline/scripts/status.mjs +295 -0
- package/skills/paper-status/SKILL.md +183 -0
- package/skills/paper-status/SKILL.md.spec.ts +190 -0
- package/skills/paper-status/paper-status.eval.mjs +22 -0
- package/skills/paper-status/paper-status.harness.mjs +25 -0
- package/skills/pc-panel-review/SKILL.md +263 -0
- package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
- package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
- package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
- package/skills/plan-paper-timeline/SKILL.md +182 -0
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
- package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
- package/skills/render-paper/SKILL.md +159 -0
- package/skills/render-paper/SKILL.md.spec.ts +166 -0
- package/skills/render-paper/check-render.sh +419 -0
- package/skills/render-paper/checkers-requirements.txt +55 -0
- package/skills/render-paper/ensure-checkers.sh +69 -0
- package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
- package/skills/render-paper/extract-pdf-facts.mjs +144 -0
- package/skills/render-paper/render-paper.eval.mjs +19 -0
- package/skills/render-paper/render-paper.harness.mjs +339 -0
- package/skills/research-ideate/SKILL.md +136 -0
- package/skills/research-ideate/SKILL.md.spec.ts +152 -0
- package/skills/research-ideate/research-ideate.eval.mjs +19 -0
- package/skills/research-ideate/research-ideate.harness.mjs +23 -0
- package/skills/skill-contract.mutations.mjs +179 -0
- package/skills/study-accepted-papers/SKILL.md +206 -0
- package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
- package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
- package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
- package/skills/submit-paper/SKILL.md +182 -0
- package/skills/submit-paper/SKILL.md.spec.ts +199 -0
- package/skills/submit-paper/check-deanon.sh +149 -0
- package/skills/submit-paper/references/publishers/acm.md +92 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
- package/skills/submit-paper/references/venues/agenticdev.md +139 -0
- package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
- package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
- package/skills/submit-paper/references/venues/aisec.md +105 -0
- package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
- package/skills/submit-paper/references/venues/realm.jsonc +81 -0
- package/skills/submit-paper/references/venues/realm.md +155 -0
- package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
- package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
- package/skills/submit-paper/submit-paper.eval.mjs +19 -0
- package/skills/submit-paper/submit-paper.harness.mjs +23 -0
- package/skills/sweep-design-space/SKILL.md +269 -0
- package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
- package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
- package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
- package/skills/tighten-paper/SKILL.md +368 -0
- package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
- package/skills/tighten-paper/structure.mjs +371 -0
- package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
- package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
- package/skills/verify-citations/SKILL.md +328 -0
- package/skills/verify-citations/SKILL.md.spec.ts +345 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
- package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
- package/skills/verify-citations/verify-citations.eval.mjs +19 -0
- package/skills/verify-citations/verify-citations.harness.mjs +23 -0
- package/src/CLAUDE.md +51 -0
- package/src/action-ref.test.ts +26 -0
- package/src/action-ref.ts +15 -0
- package/src/adapters/banal/failure.test.ts +63 -0
- package/src/adapters/banal/failure.ts +118 -0
- package/src/adapters/banal/index.test.ts +119 -0
- package/src/adapters/banal/index.ts +100 -0
- package/src/adapters/banal/install.test.ts +20 -0
- package/src/adapters/banal/install.ts +41 -0
- package/src/adapters/banal/invocation.test.ts +74 -0
- package/src/adapters/banal/invocation.ts +95 -0
- package/src/adapters/banal/locate.test.ts +52 -0
- package/src/adapters/banal/locate.ts +84 -0
- package/src/adapters/banal/output.test.ts +140 -0
- package/src/adapters/banal/output.ts +141 -0
- package/src/adapters/banal/pin.ts +30 -0
- package/src/adapters/banal/probe.ts +35 -0
- package/src/adapters/banal/run.test.ts +191 -0
- package/src/adapters/banal/run.ts +244 -0
- package/src/adapters/banal/settings.test.ts +31 -0
- package/src/adapters/banal/settings.ts +55 -0
- package/src/adapters/banal/xml.test.ts +111 -0
- package/src/adapters/banal/xml.ts +112 -0
- package/src/adapters/curl/download.io.ts +73 -0
- package/src/adapters/curl/download.test.ts +55 -0
- package/src/adapters/curl/index.ts +5 -0
- package/src/adapters/memory/index.ts +131 -0
- package/src/adapters/node/files.io.ts +39 -0
- package/src/adapters/node/files.test.ts +28 -0
- package/src/adapters/node/host.io.ts +15 -0
- package/src/adapters/node/index.ts +36 -0
- package/src/adapters/node/process.io.ts +49 -0
- package/src/adapters/node/process.test.ts +46 -0
- package/src/adapters/node/workspace.io.ts +40 -0
- package/src/adapters/node/workspace.test.ts +58 -0
- package/src/adapters/pdfjs/fill.test.ts +111 -0
- package/src/adapters/pdfjs/fill.ts +141 -0
- package/src/build-engine.harness.mjs +314 -0
- package/src/build-engine.ts +219 -0
- package/src/build.harness.mjs +631 -0
- package/src/build.mutations.mjs +195 -0
- package/src/build.ts +793 -0
- package/src/cli.harness.mjs +2007 -0
- package/src/cli.mutations.mjs +448 -0
- package/src/cli.ts +1189 -0
- package/src/doctor.harness.mjs +396 -0
- package/src/doctor.mutations.mjs +175 -0
- package/src/doctor.ts +356 -0
- package/src/domain/geometry.ts +108 -0
- package/src/domain/host.ts +23 -0
- package/src/domain/page-layout.ts +32 -0
- package/src/domain/paths.ts +5 -0
- package/src/domain/result.test.ts +26 -0
- package/src/domain/result.ts +29 -0
- package/src/domain/sha256.test.ts +12 -0
- package/src/domain/sha256.ts +21 -0
- package/src/domain/text.ts +11 -0
- package/src/engine.harness.mjs +252 -0
- package/src/engine.ts +176 -0
- package/src/exit-code.test.ts +21 -0
- package/src/exit-code.ts +38 -0
- package/src/facts-file.test.ts +240 -0
- package/src/facts-file.ts +241 -0
- package/src/hooks-settings.harness.mjs +386 -0
- package/src/hooks-settings.mutations.mjs +116 -0
- package/src/hooks-settings.ts +434 -0
- package/src/init.ts +900 -0
- package/src/latex-log.harness.mjs +226 -0
- package/src/latex-log.ts +234 -0
- package/src/latex-loop.harness.mjs +449 -0
- package/src/latex-loop.ts +211 -0
- package/src/link-skills.harness.mjs +273 -0
- package/src/link-skills.mutations.mjs +136 -0
- package/src/link-skills.ts +258 -0
- package/src/new-paper.harness.mjs +216 -0
- package/src/new-paper.mutations.mjs +79 -0
- package/src/new-paper.ts +158 -0
- package/src/pdf-facts.harness.mjs +188 -0
- package/src/pdf-facts.ts +327 -0
- package/src/pdf-geometry.harness.mjs +254 -0
- package/src/pdf-geometry.ts +300 -0
- package/src/ports/download.ts +10 -0
- package/src/ports/files.ts +11 -0
- package/src/ports/measure-geometry.ts +8 -0
- package/src/ports/process.ts +46 -0
- package/src/ports/tool-installer.ts +33 -0
- package/src/ports/workspace.ts +20 -0
- package/src/rules-config.harness.mjs +114 -0
- package/src/rules-config.ts +178 -0
- package/src/structure.harness.mjs +179 -0
- package/src/structure.mutations.mjs +83 -0
- package/src/structure.ts +166 -0
- package/src/tex-requirements.harness.mjs +238 -0
- package/src/tex-requirements.ts +181 -0
- package/src/toolchain.harness.mjs +651 -0
- package/src/toolchain.ts +755 -0
- package/src/types.ts +106 -0
- package/templates/paper/PIPELINE-STATUS.md +72 -0
- package/templates/paper/paper.md +4 -0
- package/templates/paper/paper.tex +8 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-checks.mjs — everything asserted about ONE skill, as a function you call with its name.
|
|
3
|
+
* `checkSkill("verify-citations")`.
|
|
4
|
+
*
|
|
5
|
+
* WHY A FUNCTION AND NOT A HARNESS. Until 2026-08-11 these assertions lived in
|
|
6
|
+
* `skills.harness.mjs`, a 614-line file named after no surface, and the 21 colocated per-skill
|
|
7
|
+
* harnesses reached it by setting `PIPELINE_SKILL_ONLY` and importing it for its side effects.
|
|
8
|
+
* That is a script pretending to be an API: the caller could not pass an argument, could not get
|
|
9
|
+
* a result, and the coupling was an environment variable. The owner asked three times why the
|
|
10
|
+
* file survived; the answer was that I had wrapped it instead of splitting it.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IS CHECKED HERE vs NEXT DOOR. Anything decidable from one SKILL.md: strict-YAML
|
|
13
|
+
* frontmatter, the declared tool contract, every script path it instructs, whether it is
|
|
14
|
+
* PERMITTED to run what it instructs, whether it files under its OWN identity, and its verdict
|
|
15
|
+
* vocabulary. Anything about the SET — name uniqueness, the gate table, the exclusion list — is
|
|
16
|
+
* `pipeline-corpus.harness.mjs`, because a per-skill run structurally cannot see it.
|
|
17
|
+
*
|
|
18
|
+
* 🔴 KNOWN-RED ASSERTIONS ARE DEFERRED, NOT WEAKENED, and the deferral list is now PER CALL.
|
|
19
|
+
* A throw would abort the rest, making one open finding silently skip every check after it —
|
|
20
|
+
* the failure this whole directory exists to end. Per call matters too: with one shared list,
|
|
21
|
+
* a full sweep could attribute one skill's failures to another's line in the report.
|
|
22
|
+
*/
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { recordCheck } from "vigiles";
|
|
27
|
+
|
|
28
|
+
// Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). `requireMarkdown()` is called by
|
|
29
|
+
// `skill-corpus.mjs` on the import below, so by the time anything is parsed here the parser
|
|
30
|
+
// either exists or the process does not.
|
|
31
|
+
import { headings as mdHeadings } from "./markdown.mjs";
|
|
32
|
+
|
|
33
|
+
// 🔴 2026-08-28: nine names were removed from this list, and that is NOT a style cleanup — they
|
|
34
|
+
// had drifted apart from what the file does. `EXCLUDED PIPELINE_MARKER GATE_SKILLS EXPECTED_GATES
|
|
35
|
+
// KINDS parseGate` are about the SET of skills (name uniqueness, the gate table, the exclusion
|
|
36
|
+
// list); the file's own header says that is `pipeline-corpus.harness.mjs`'s job, and it was
|
|
37
|
+
// verified by grep — all six really are used there, no logic was lost. `frontmatter` and
|
|
38
|
+
// `toolList` are called INSIDE `load()` in `skill-corpus.mjs`, and the direct import was a
|
|
39
|
+
// second path to the same data. `allBold` is the raw material for `namedKinds`/`namedRetired`,
|
|
40
|
+
// which are imported right beside it.
|
|
41
|
+
import {
|
|
42
|
+
SKILLS,
|
|
43
|
+
SKILLS_DIR,
|
|
44
|
+
RETIRED,
|
|
45
|
+
ABSTENTIONS,
|
|
46
|
+
record,
|
|
47
|
+
ROOT,
|
|
48
|
+
LEDGER_ONLY_BASH,
|
|
49
|
+
recordBlock,
|
|
50
|
+
namedKinds,
|
|
51
|
+
namedRetired,
|
|
52
|
+
load,
|
|
53
|
+
commands,
|
|
54
|
+
permits,
|
|
55
|
+
definedKinds,
|
|
56
|
+
namedReasons,
|
|
57
|
+
ADMISSION,
|
|
58
|
+
tmp,
|
|
59
|
+
} from "./skill-corpus.mjs";
|
|
60
|
+
|
|
61
|
+
// ═════════════════════════════════════════════════════════════════════════════════════════════
|
|
62
|
+
// WHERE THE PIPELINE'S SCRIPTS ARE — ASKED, NOT SPELLED (2026-09-12)
|
|
63
|
+
//
|
|
64
|
+
// Until today this file carried the string `.claude/skills/paper-pipeline/scripts/` in SEVEN
|
|
65
|
+
// places: two announce/record templates, two `indexOf` ordering probes, one `startsWith` filter
|
|
66
|
+
// and two regular expressions. Every one of them is this consumer's own layout, and a mechanism
|
|
67
|
+
// that spells one consumer's layout has exactly one possible user — the thing the extraction
|
|
68
|
+
// exists to undo. Nine colocated harnesses of wave ① import THIS module and nothing else from
|
|
69
|
+
// `lib/`, so until the seven literals left, none of them could be built anywhere but here.
|
|
70
|
+
//
|
|
71
|
+
// The value now comes from the package, as the fourth carrier beside `papersRoot()` (where the
|
|
72
|
+
// papers are), `ledgerPath()` (where the journal is) and `citeChecks` (where the consumer's own
|
|
73
|
+
// citation checkers are). One declaration in `package.json`, default
|
|
74
|
+
// `.claude/skills/paper-pipeline/scripts`, and the resolver REFUSES a root that is not on disk
|
|
75
|
+
// or that resolves inside `node_modules`.
|
|
76
|
+
//
|
|
77
|
+
// 🔴 WHY THE REFUSAL IS THE POINT AND NOT DEFENSIVENESS. Six of the seven uses are FILTERS —
|
|
78
|
+
// `startsWith`, `indexOf`, `RegExp.test`. A wrong prefix does not make them wrong; it makes them
|
|
79
|
+
// match nothing, so every loop body below goes unentered and `checkSkill` reports a clean skill
|
|
80
|
+
// it never examined. That failure is byte-identical to success, and a throw while this module is
|
|
81
|
+
// still LOADING is the only place it can be made visible.
|
|
82
|
+
//
|
|
83
|
+
// `{ env: {}, cwd: ROOT }` rather than the defaults: `ROOT` above already resolved
|
|
84
|
+
// `CLAUDE_PROJECT_DIR`, and letting the resolver consult it again would make the two disagree on
|
|
85
|
+
// exactly one input (an empty-string variable — `??` keeps it, `||` does not).
|
|
86
|
+
import {
|
|
87
|
+
installedSkills,
|
|
88
|
+
pipelineScripts,
|
|
89
|
+
scriptsRoot,
|
|
90
|
+
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
91
|
+
|
|
92
|
+
const PIPELINE = pipelineScripts(scriptsRoot({ env: {}, cwd: ROOT }));
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A literal path, embedded in a RegExp. Not a parser dodge — the subject here IS a string (a
|
|
96
|
+
* path, which has no grammar to ask), and this is the standard way to put one inside a pattern.
|
|
97
|
+
*/
|
|
98
|
+
const reEscape = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
+
|
|
100
|
+
/** `Bash(node <pipeline scripts>/<name>.mjs:*)` — the narrowed entry the ledger-only skills get. */
|
|
101
|
+
const LEDGER_BASH_RE = new RegExp(
|
|
102
|
+
`^Bash\\(node ${reEscape(PIPELINE.prefix)}[a-z-]+\\.mjs:\\*\\)$`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A command that is one of the two ledger calls.
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ DELIBERATELY NOT ANCHORED AT THE START, because the literal it replaced was not either. The
|
|
109
|
+
* anchored form would be tighter and is probably what anyone would write today, but tightening it
|
|
110
|
+
* in the same change that parameterises it would mean a `before = after` comparison could no
|
|
111
|
+
* longer tell a successful parameterisation from a changed rule.
|
|
112
|
+
*/
|
|
113
|
+
const LEDGER_SCRIPT_RE = new RegExp(
|
|
114
|
+
`${reEscape(PIPELINE.prefix)}(announce|ledger)\\.mjs$`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Assert everything single-skill about `name`. Throws with EVERY finding, not the first.
|
|
119
|
+
* Returns the number of skills checked (1) so a caller can report what it actually did.
|
|
120
|
+
*/
|
|
121
|
+
export async function checkSkill(name) {
|
|
122
|
+
if (!SKILLS.includes(name))
|
|
123
|
+
throw new Error(
|
|
124
|
+
`"${name}" is not a wired pipeline skill. Known: ${SKILLS.join(", ")}`,
|
|
125
|
+
);
|
|
126
|
+
const deferred = [];
|
|
127
|
+
const soft = (fn) => {
|
|
128
|
+
try {
|
|
129
|
+
fn();
|
|
130
|
+
} catch (e) {
|
|
131
|
+
deferred.push(e);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const skills = [load(name)];
|
|
135
|
+
const skillDirs = new Set(installedSkills(SKILLS_DIR));
|
|
136
|
+
|
|
137
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
// 1. GONE, AND THAT IS THE RESULT (2026-08-19). This slot held a strict-YAML assertion whose whole
|
|
139
|
+
// job was to police the gap between how the corpus was READ here (a hand-rolled lenient reader)
|
|
140
|
+
// and how every other consumer reads it (a real YAML parser). The corpus owner, on being shown the mutation
|
|
141
|
+
// suite: "sounds like a hack… let's think about how to do it properly". The right fix was not a better
|
|
142
|
+
// check — it was to stop being lenient: the two remaining unparseable descriptions were quoted and
|
|
143
|
+
// `parseFm` in skill-corpus.mjs now throws on anything a real parser rejects.
|
|
144
|
+
//
|
|
145
|
+
// So the defect class is not detected here any more; it is UNREACHABLE. A skill whose frontmatter
|
|
146
|
+
// is not valid YAML does not reach these assertions with a plausible-looking object and a missing
|
|
147
|
+
// `allowed-tools` — it does not load at all, and says why. What the corpus does about the root
|
|
148
|
+
// cause (`vigiles compile` interpolating the description into YAML raw) is written where the
|
|
149
|
+
// parser lives.
|
|
150
|
+
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
152
|
+
// 2-4. Identity and contract fields, read from the strict parse — there is no other kind now, and
|
|
153
|
+
// nothing above can fail while leaving these reachable. `name` is what the ledger and `skillHash()`
|
|
154
|
+
// key on: a name that disagrees
|
|
155
|
+
// with its directory means every verdict this skill records hashes against a directory that is not
|
|
156
|
+
// its own, and staleness detection quietly stops working for it.
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
for (const s of skills) {
|
|
159
|
+
soft(() =>
|
|
160
|
+
assert.equal(
|
|
161
|
+
s.fm.name,
|
|
162
|
+
s.name,
|
|
163
|
+
`${s.name}: frontmatter name is ${JSON.stringify(s.fm.name)} but the directory is "${s.name}". ` +
|
|
164
|
+
`skillHash() resolves by DIRECTORY and the ledger stores whatever the SKILL.md text passes — ` +
|
|
165
|
+
`disagreement here splits one skill's history across two keys and neither is ever fresh.`,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
soft(() =>
|
|
170
|
+
assert.ok(
|
|
171
|
+
(s.fm.description || "").length > 40,
|
|
172
|
+
`${s.name}: description is missing or trivially short (${(s.fm.description || "").length} chars). ` +
|
|
173
|
+
`The description is the ONLY thing the model matches on when choosing a skill — an empty one is ` +
|
|
174
|
+
`a skill that never fires, and a skill that never fires is indistinguishable from one that works.`,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
soft(() =>
|
|
179
|
+
assert.ok(
|
|
180
|
+
s.tools.length > 0,
|
|
181
|
+
`${s.name}: no allowed-tools. A skill declaring nothing INHERITS EVERYTHING — Bash, WebFetch and ` +
|
|
182
|
+
`every MCP server (Calendar, GitHub-write, Vercel) — over a repo holding health data, DNA and ` +
|
|
183
|
+
`bank balances. This is the finding the 2026-08-03 audit closed for all 35 skills.`,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
189
|
+
// 5-6. The two wired blocks: `## Run me` (announce, first) and `Record the verdict` (ledger, last).
|
|
190
|
+
// The README calls these the whole wiring contract. Each is checked three ways — the block exists,
|
|
191
|
+
// the script it names exists on disk, and the SKILL NAME IT PASSES IS ITS OWN. The third is the
|
|
192
|
+
// copy-paste defect: these blocks differ from their siblings' by one token, and `announce.mjs`
|
|
193
|
+
// validates nothing, so a stale token records a real verdict under another skill's name.
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
195
|
+
for (const s of skills) {
|
|
196
|
+
const cmds = commands(s.src);
|
|
197
|
+
|
|
198
|
+
// 2026-08-11: "is there a `## Run me` block" is a question about a HEADING, therefore about
|
|
199
|
+
// level and text, not about the number of hashes. The former `/^#{1,3} Run me\s*$/m` also
|
|
200
|
+
// counted a `## Run me` line inside a ```-block as a heading: a skill SHOWING someone else's
|
|
201
|
+
// block in an example passed the check without having one of its own. Silence is a soft
|
|
202
|
+
// assertion's success state, so there was nothing to notice this with.
|
|
203
|
+
soft(() =>
|
|
204
|
+
assert.ok(
|
|
205
|
+
mdHeadings(s.src).some((h) => h.depth <= 3 && h.text === "Run me"),
|
|
206
|
+
`${s.name}: no "## Run me" block. Without it the skill leaves no trace of having started, and ` +
|
|
207
|
+
`an advisory pass that leaves no trace cannot be observed failing — silence is both its error ` +
|
|
208
|
+
`state and its normal state. Three hooks in this repo died exactly that way.`,
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
for (const [label, script, want] of [
|
|
213
|
+
["announce", PIPELINE.announce, "Run me"],
|
|
214
|
+
["record", PIPELINE.ledger, "Record the verdict"],
|
|
215
|
+
]) {
|
|
216
|
+
const hits = cmds.filter((c) => c.script === script);
|
|
217
|
+
soft(() =>
|
|
218
|
+
assert.ok(
|
|
219
|
+
hits.length > 0,
|
|
220
|
+
`${s.name}: the "${want}" block instructs no \`node ${script}\` command. The block may be present ` +
|
|
221
|
+
`and empty — the README's contract is the CALL, not the heading.`,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
for (const h of hits) {
|
|
225
|
+
soft(() =>
|
|
226
|
+
assert.ok(
|
|
227
|
+
existsSync(join(ROOT, h.script)),
|
|
228
|
+
`${s.name}: instructs \`${h.cmd}\` and ${h.script} DOES NOT EXIST. A skill naming a path that ` +
|
|
229
|
+
`is not there is a dead instruction: the model reads it, does something adjacent, and reports ` +
|
|
230
|
+
`success. Prose cannot notice this; nothing else in this repo was looking.`,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
// arg[0] for announce is the skill; for ledger.mjs it is the literal `record`, then the skill.
|
|
234
|
+
const passed =
|
|
235
|
+
label === "announce"
|
|
236
|
+
? h.args[0]
|
|
237
|
+
: h.args[0] === "record"
|
|
238
|
+
? h.args[1]
|
|
239
|
+
: h.args[0];
|
|
240
|
+
soft(() =>
|
|
241
|
+
assert.equal(
|
|
242
|
+
passed,
|
|
243
|
+
s.name,
|
|
244
|
+
`${s.name}: its ${label} command files under ${JSON.stringify(passed)}, not "${s.name}" — ` +
|
|
245
|
+
`\`${h.cmd}\`. These blocks are copy-paste templates differing in one token and announce.mjs ` +
|
|
246
|
+
`validates nothing, so this records a real run under a SIBLING'S identity: the wrong gate ` +
|
|
247
|
+
`reads FRESH in status.mjs and the right one reads NEVER-RUN.`,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
soft(() => {
|
|
254
|
+
const a = s.src.indexOf(PIPELINE.announce);
|
|
255
|
+
const r = s.src.indexOf(`${PIPELINE.ledger} record`);
|
|
256
|
+
if (a >= 0 && r >= 0)
|
|
257
|
+
assert.ok(
|
|
258
|
+
a < r,
|
|
259
|
+
`${s.name}: the ledger record call appears BEFORE the announce call. The contract is announce ` +
|
|
260
|
+
`first / record last; a verdict written before the run started is a verdict about nothing.`,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
soft(() =>
|
|
265
|
+
assert.ok(
|
|
266
|
+
recordBlock(s.src),
|
|
267
|
+
`${s.name}: no "Record the verdict" heading. A run with an announce row and no verdict row is ` +
|
|
268
|
+
`indistinguishable from a skill that started and died.`,
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
274
|
+
// 7. EVERY script path the skill instructs resolves — not only the two ledger calls.
|
|
275
|
+
//
|
|
276
|
+
// Scoped to paths that are unambiguously resolvable: `.claude/…` is repo-root-relative and
|
|
277
|
+
// `scripts/…` is skill-relative (both conventions are used consistently across the corpus). Paths
|
|
278
|
+
// like `repro/build-submission.sh` are relative to a PAPER directory that does not exist at test
|
|
279
|
+
// time, so they are excluded BY NAME rather than by a guess that would fire on correct text.
|
|
280
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
281
|
+
for (const s of skills) {
|
|
282
|
+
for (const c of commands(s.src)) {
|
|
283
|
+
const abs = c.script.startsWith(".claude/")
|
|
284
|
+
? join(ROOT, c.script)
|
|
285
|
+
: c.script.startsWith("scripts/")
|
|
286
|
+
? join(SKILLS_DIR, s.name, c.script)
|
|
287
|
+
: null;
|
|
288
|
+
if (!abs) continue;
|
|
289
|
+
soft(() =>
|
|
290
|
+
assert.ok(
|
|
291
|
+
existsSync(abs),
|
|
292
|
+
`${s.name}: instructs \`${c.cmd}\` but ${c.script} does not exist (looked at ${abs}). Dead ` +
|
|
293
|
+
`instruction — the model will improvise something adjacent and report it as done.`,
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
300
|
+
// 8. A skill is PERMITTED TO RUN what it instructs. The inverse-direction risk of narrowing: over-
|
|
301
|
+
// narrow an allowed-tools entry and the skill's own first instruction is denied at runtime, which
|
|
302
|
+
// looks like the skill silently declining to announce — the same invisible failure as a dead hook,
|
|
303
|
+
// arrived at from the opposite direction. Nothing else checks the two halves against each other.
|
|
304
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
305
|
+
for (const s of skills) {
|
|
306
|
+
for (const c of commands(s.src)) {
|
|
307
|
+
// The calls the pipeline contract owns. Was `.claude/pipeline/` until 2026-08-15, when that
|
|
308
|
+
// directory was dissolved and `ledger.mjs`/`announce.mjs` went home to the skill they serve.
|
|
309
|
+
if (!c.script.startsWith(PIPELINE.prefix)) continue;
|
|
310
|
+
soft(() =>
|
|
311
|
+
assert.ok(
|
|
312
|
+
s.tools.some((t) => permits(t, c.cmd)),
|
|
313
|
+
`${s.name}: instructs \`${c.cmd}\` but its allowed-tools [${s.tools.join(", ")}] permit no such ` +
|
|
314
|
+
`Bash command. The instruction is denied at runtime and the skill simply does not announce or ` +
|
|
315
|
+
`record — invisible, because a gate that never wrote a row looks exactly like one nobody ran.`,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
322
|
+
// 9. The nine ledger-only skills keep their Bash NARROWED.
|
|
323
|
+
//
|
|
324
|
+
// A plain `Bash` on one of these is the regression that took this repo's Safety score from 70 to 0
|
|
325
|
+
// in the audit's own arithmetic: Read + Bash + WebFetch is the lethal trifecta, and these nine are
|
|
326
|
+
// analysis skills that read the knowledge base. `argument-arc` grading a paper does not need a
|
|
327
|
+
// shell; it needs to append one line to a JSONL. Construction beats prose — with the narrow entry
|
|
328
|
+
// the capability is ABSENT, and absence cannot be talked around by injected text in a fetched CFP.
|
|
329
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
330
|
+
for (const s of skills.filter((s) => LEDGER_ONLY_BASH.has(s.name))) {
|
|
331
|
+
const bash = s.tools.filter((t) => t === "Bash" || t.startsWith("Bash("));
|
|
332
|
+
soft(() =>
|
|
333
|
+
assert.ok(
|
|
334
|
+
bash.length > 0,
|
|
335
|
+
`${s.name}: listed as ledger-only-Bash but declares no Bash entry at all — it cannot run its own ` +
|
|
336
|
+
`announce/record calls. Either add the narrow entries or remove it from LEDGER_ONLY_BASH here.`,
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
for (const b of bash) {
|
|
340
|
+
soft(() =>
|
|
341
|
+
assert.match(
|
|
342
|
+
b,
|
|
343
|
+
LEDGER_BASH_RE,
|
|
344
|
+
`${s.name}: Bash entry ${JSON.stringify(b)} is not narrowed to the ledger. This skill gained Bash ` +
|
|
345
|
+
`ONLY to append a run row; a bare \`Bash\` hands a knowledge-base reader a shell and completes ` +
|
|
346
|
+
`the lethal trifecta. See ALLOWED-TOOLS-AUDIT-2026-08-03.md §2 for why this one was narrowed.`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
353
|
+
// 10. THE VOCABULARY, TURNED AROUND ON 2026-08-10.
|
|
354
|
+
//
|
|
355
|
+
// WHAT THIS USED TO ASSERT, and why it had to change rather than be deleted. The old text was:
|
|
356
|
+
// "the vocabulary contains a REACHABLE NEGATIVE — or the text admits it does not", because a gate
|
|
357
|
+
// documented as PASS-only cannot say no and no amount of running would reveal it. With `PASS`
|
|
358
|
+
// deleted from the ledger, the literal form of that assertion is VACUOUS: `FINDING` and
|
|
359
|
+
// `ABSTAINED` are the only constructors, so every conforming block names a negative by
|
|
360
|
+
// construction and the check would pass on every file forever while reading as though it still
|
|
361
|
+
// guarded something. A vacuous assertion is worse than a missing one — it is a green light with
|
|
362
|
+
// nothing behind it, which is this directory's whole subject.
|
|
363
|
+
//
|
|
364
|
+
// So the property is carried over rather than the wording. It was never really "names a negative";
|
|
365
|
+
// it was **"this check is capable of reporting that something is wrong."** Under the new
|
|
366
|
+
// vocabulary that is: the block names FINDING, the evidence-carrying constructor. A block that
|
|
367
|
+
// documents only how to abstain describes a check that cannot fail — the same defect, wearing the
|
|
368
|
+
// new words.
|
|
369
|
+
//
|
|
370
|
+
// 10b is genuinely new and is the migration guard: a block still instructing a RETIRED constructor
|
|
371
|
+
// tells the model to run a command that now exits 2. Prose survives refactors that code does not,
|
|
372
|
+
// and 22 files were rewritten by hand today.
|
|
373
|
+
//
|
|
374
|
+
// 10c requires a named abstention REASON, because `ABSTAINED` with no reason is the deleted `PASS`
|
|
375
|
+
// with a longer name — the closed reason vocabulary is the entire mechanism separating "ran and
|
|
376
|
+
// found nothing" from "could not run".
|
|
377
|
+
//
|
|
378
|
+
// The escape hatch on 10a is deliberate and is the honest case, not a loophole: `draft-paper`
|
|
379
|
+
// GENERATES, will abstain nearly always, and its text says so in status.mjs's own terms. That
|
|
380
|
+
// admission is load-bearing documentation — delete it and an explained limitation becomes an
|
|
381
|
+
// unexplained never-finding check — so it is asserted directly, not merely permitted.
|
|
382
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
383
|
+
for (const s of skills) {
|
|
384
|
+
const blk = recordBlock(s.src);
|
|
385
|
+
if (!blk) continue;
|
|
386
|
+
|
|
387
|
+
soft(() =>
|
|
388
|
+
assert.ok(
|
|
389
|
+
namedKinds(blk).includes("FINDING") || ADMISSION.test(blk),
|
|
390
|
+
`${s.name}: its record block names [${namedKinds(blk).join(", ") || "nothing"}] and not FINDING, ` +
|
|
391
|
+
`and does not admit that it cannot produce one. FINDING is the only constructor that carries ` +
|
|
392
|
+
`evidence; a block documenting only how to ABSTAIN describes a check that can never report that ` +
|
|
393
|
+
`anything is wrong. Either name the finding this skill can return, or state plainly that it ` +
|
|
394
|
+
`generates rather than judges — as draft-paper does.`,
|
|
395
|
+
),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
soft(() =>
|
|
399
|
+
assert.deepEqual(
|
|
400
|
+
namedRetired(blk),
|
|
401
|
+
[],
|
|
402
|
+
`${s.name}: its record block still instructs the RETIRED constructor(s) ` +
|
|
403
|
+
`[${namedRetired(blk).join(", ")}]. ${[...RETIRED.keys()]
|
|
404
|
+
.filter((k) => namedRetired(blk).includes(k))
|
|
405
|
+
.map((k) => `${k} ${RETIRED.get(k)}`)
|
|
406
|
+
.join(" ")} ` +
|
|
407
|
+
`A model following this block runs a command that exits 2 at the last step of finished work.`,
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
soft(() =>
|
|
412
|
+
assert.ok(
|
|
413
|
+
namedReasons(blk).length > 0,
|
|
414
|
+
`${s.name}: its record block names no abstention reason from [${[...ABSTENTIONS.keys()].join(", ")}]. ` +
|
|
415
|
+
`An ABSTAINED with an unnamed reason is the deleted PASS with more characters: the closed reason ` +
|
|
416
|
+
`set is the only thing separating "ran and found nothing" from "could not run", and a block that ` +
|
|
417
|
+
`does not say which one applies leaves the model to invent a string record() will refuse.`,
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
// Guarded for one-skill mode: this assertion is ABOUT draft-paper, so it only runs
|
|
422
|
+
// when draft-paper is in scope. Silently skipping it in a full sweep would be the
|
|
423
|
+
// vacuity this file hunts, so the guard is the membership test, not a try/catch.
|
|
424
|
+
const draftPaper = skills.find((s) => s.name === "draft-paper");
|
|
425
|
+
if (draftPaper)
|
|
426
|
+
soft(() =>
|
|
427
|
+
assert.match(
|
|
428
|
+
recordBlock(draftPaper.src),
|
|
429
|
+
ADMISSION,
|
|
430
|
+
`draft-paper: the honest admission is gone from its record block. It is the one skill in the ` +
|
|
431
|
+
`pipeline that will almost always abstain, and the paragraph saying so — in status.mjs's own ` +
|
|
432
|
+
`terms, "a generator is not a gate" — is what keeps that flag readable as a known limitation ` +
|
|
433
|
+
`rather than an unexplained never-finding check someone later "fixes" by faking one.`,
|
|
434
|
+
),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
438
|
+
// 11. No skill instructs something `record()` will throw on — proven by CALLING IT, not by reading
|
|
439
|
+
// the vocabulary list. `record()` validates its argument and throws; a skill telling the model to
|
|
440
|
+
// record `GREEN`, `OK` or `PASS` produces a crash at the last step of a completed pass, after the
|
|
441
|
+
// work is done.
|
|
442
|
+
//
|
|
443
|
+
// Each constructor is called in its VALID shape, because the shape is part of the contract: a
|
|
444
|
+
// FINDING needs a count and an existing report, an ABSTENTION needs a reason from the closed set.
|
|
445
|
+
// Calling `record` with a bare kind would prove only that the kind string is spelled right.
|
|
446
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
447
|
+
{
|
|
448
|
+
writeFileSync(
|
|
449
|
+
join(tmp, "fixture-report.md"),
|
|
450
|
+
"---\nfindings: 1\n---\n\nharness fixture\n",
|
|
451
|
+
);
|
|
452
|
+
for (const s of skills) {
|
|
453
|
+
const blk = recordBlock(s.src);
|
|
454
|
+
if (!blk) continue;
|
|
455
|
+
for (const v of definedKinds(blk)) {
|
|
456
|
+
soft(() =>
|
|
457
|
+
record(
|
|
458
|
+
v === "ABSTAINED"
|
|
459
|
+
? {
|
|
460
|
+
skill: s.name,
|
|
461
|
+
paper: tmp,
|
|
462
|
+
kind: v,
|
|
463
|
+
reason: namedReasons(blk)[0] ?? null,
|
|
464
|
+
note: "harness fixture",
|
|
465
|
+
}
|
|
466
|
+
: {
|
|
467
|
+
skill: s.name,
|
|
468
|
+
paper: tmp,
|
|
469
|
+
kind: v,
|
|
470
|
+
findings: 1,
|
|
471
|
+
report: "fixture-report.md",
|
|
472
|
+
note: "harness fixture",
|
|
473
|
+
},
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Every skill name any of these files hands to announce.mjs or ledger.mjs must resolve to a real
|
|
480
|
+
// skill, including names inside worked examples. announce.mjs does NOT validate: an unknown name
|
|
481
|
+
// hashes to `????????` and the row is written anyway.
|
|
482
|
+
for (const s of skills) {
|
|
483
|
+
for (const c of commands(s.src)) {
|
|
484
|
+
if (!LEDGER_SCRIPT_RE.test(c.script)) continue;
|
|
485
|
+
const passed = c.script.endsWith("announce.mjs")
|
|
486
|
+
? c.args[0]
|
|
487
|
+
: c.args[0] === "record"
|
|
488
|
+
? c.args[1]
|
|
489
|
+
: null;
|
|
490
|
+
if (!passed || passed.startsWith("<")) continue;
|
|
491
|
+
soft(() =>
|
|
492
|
+
assert.ok(
|
|
493
|
+
skillDirs.has(passed),
|
|
494
|
+
`${s.name}: instructs \`${c.cmd}\`, naming skill "${passed}", which has no directory under ` +
|
|
495
|
+
`.claude/skills/. announce.mjs validates nothing — it hashes the unknown name to "????????" ` +
|
|
496
|
+
`and writes the row, so the typo is permanent and silent in the ledger.`,
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (deferred.length) {
|
|
502
|
+
const e = new Error(
|
|
503
|
+
`${deferred.length} assertion(s) failed for pipeline skill "${name}":\n\n` +
|
|
504
|
+
deferred.map((d, i) => ` [${i + 1}] ${d.message}`).join("\n\n"),
|
|
505
|
+
);
|
|
506
|
+
e.stack = e.message;
|
|
507
|
+
throw e;
|
|
508
|
+
}
|
|
509
|
+
// Reported so `vigiles test` can tell a run that verified this skill from a file that merely
|
|
510
|
+
// sits beside it — the distinction colocation cannot make (see vigiles check-count.ts).
|
|
511
|
+
recordCheck();
|
|
512
|
+
console.log(
|
|
513
|
+
`✓ ${name}: frontmatter, wiring, script paths, tool contract, verdict vocabulary`,
|
|
514
|
+
);
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ═════════════════════════════════════════════════════════════════════════════════════════════════
|
|
519
|
+
// checkProseSkill — for skills WITHOUT the pipeline (2026-08-27)
|
|
520
|
+
//
|
|
521
|
+
// WHY A SEPARATE FUNCTION AND NOT A FLAG ON checkSkill. `checkSkill` requires `## Run me`, a call
|
|
522
|
+
// to `announce.mjs` and a "Record the verdict" block — those are paper-pipeline conventions, and a
|
|
523
|
+
// skill like `avito-posting` neither has them nor should. Running it through checkSkill means
|
|
524
|
+
// getting three red assertions about the absence of what it never promised. A check that is red by
|
|
525
|
+
// construction lives one day, after which it gets silenced — that has already happened in this repo.
|
|
526
|
+
//
|
|
527
|
+
// 🔴 BOTH HALVES ARE BUILT IN, NOT WRITTEN BY HAND. The `CLAUDE.md` rule requires two halves of
|
|
528
|
+
// every check: it fires on a planted defect AND it stays silent on clean input. The "fires" half is
|
|
529
|
+
// written less often — it is not needed to make a run go green. So here it is not entrusted to the
|
|
530
|
+
// author: EVERY predicate below is run twice — over the real text and over the mutated one, and the
|
|
531
|
+
// disagreement of the verdicts IS the assertion. The mutation cannot be forgotten, there is nowhere
|
|
532
|
+
// to forget it.
|
|
533
|
+
//
|
|
534
|
+
// WHAT THIS DOES NOT PROVE: that the skill fires on the right request and that its advice gives a
|
|
535
|
+
// good result. The first is the trigger eval, the second is the model. Only what is decidable from
|
|
536
|
+
// the text is here.
|
|
537
|
+
// ═════════════════════════════════════════════════════════════════════════════════════════════════
|
|
538
|
+
|
|
539
|
+
/** Relative markdown links that do not resolve from the skill's directory. */
|
|
540
|
+
function danglingLinks(src, dir) {
|
|
541
|
+
const out = [];
|
|
542
|
+
for (const m of src.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) {
|
|
543
|
+
const t = m[1].split("#")[0];
|
|
544
|
+
if (!t || /^(https?:|mailto:)/.test(t)) continue;
|
|
545
|
+
if (!existsSync(join(dir, t))) out.push(t);
|
|
546
|
+
}
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Check the prose skill `name`.
|
|
552
|
+
*
|
|
553
|
+
* @param {string} name — the directory name in `.claude/skills/`
|
|
554
|
+
* @param {{mustSay?: [RegExp,string][], mustNotSay?: [RegExp,string][],
|
|
555
|
+
* toolsNeeded?: Record<string,string>}} opts
|
|
556
|
+
* `mustSay` — that whose DISAPPEARANCE is a regression (a rule obtained by measurement).
|
|
557
|
+
* `mustNotSay` — that whose APPEARANCE is a regression (a refuted claim).
|
|
558
|
+
* `toolsNeeded` — {tool: REASON}. Lifts the ban on Bash/Agent/Task for a skill that
|
|
559
|
+
* needs the tool IN SUBSTANCE. The reason is mandatory and non-empty.
|
|
560
|
+
*
|
|
561
|
+
* 🔴 Why `toolsNeeded` appeared (2026-09-01) and why it carries a REASON rather than a flag.
|
|
562
|
+
* The ban on Bash/Agent/Task was written for two text-only skills and was hard-wired as
|
|
563
|
+
* universal: "a skill that only writes text". For `session-retro` that is a lie — its own
|
|
564
|
+
* instruction prescribes SPAWNING auditors ("Spawn parallel audit subagents") and running
|
|
565
|
+
* measurements, that is, without Agent and Bash it does not do what it is.
|
|
566
|
+
*
|
|
567
|
+
* The temptation was to switch the check off for it entirely. That is wrong: the ban protects
|
|
568
|
+
* a repository with health data and balances from the third leg of the lethal trifecta, and it
|
|
569
|
+
* is still right for the rest. So it is not a flag but a NAMED exemption with a reason — the
|
|
570
|
+
* same form as `vigiles:local-by-design` in this same knowledge base: switching off is
|
|
571
|
+
* possible, but it stays written down, and the next reader sees WHAT exactly was decided.
|
|
572
|
+
*/
|
|
573
|
+
export async function checkProseSkill(name, opts = {}) {
|
|
574
|
+
const { skillContract } = await import("vigiles");
|
|
575
|
+
const dir = join(SKILLS_DIR, name);
|
|
576
|
+
const md = join(dir, "SKILL.md");
|
|
577
|
+
const deferred = [];
|
|
578
|
+
const soft = (fn) => {
|
|
579
|
+
try {
|
|
580
|
+
fn();
|
|
581
|
+
} catch (e) {
|
|
582
|
+
deferred.push(e);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
assert.ok(existsSync(md), `${name}: no ${md} — nothing to check`);
|
|
587
|
+
const src = readFileSync(md, "utf8");
|
|
588
|
+
|
|
589
|
+
// 1. The tool contract. Both skills deliberately DROPPED Bash — it was the third leg of the
|
|
590
|
+
// lethal trifecta over a repo with health data and balances. A regression here is silent: a
|
|
591
|
+
// skill with no parseable `allowed-tools` INHERITS EVERYTHING, and that looks exactly like a
|
|
592
|
+
// declared contract.
|
|
593
|
+
const c = await skillContract(dir);
|
|
594
|
+
soft(() =>
|
|
595
|
+
assert.equal(
|
|
596
|
+
c.malformed,
|
|
597
|
+
false,
|
|
598
|
+
`${name}: allowed-tools does not parse. The file LOOKS like it declares a contract, while a ` +
|
|
599
|
+
`strict parser sees nothing — the skill silently inherits every tool. Known cause: ` +
|
|
600
|
+
`\`vigiles compile\` strips the quotes off a scalar containing ": " and breaks the YAML ` +
|
|
601
|
+
`(see CLAUDE.md, measured 2026-08-19).`,
|
|
602
|
+
),
|
|
603
|
+
);
|
|
604
|
+
soft(() =>
|
|
605
|
+
assert.ok(
|
|
606
|
+
c.declared.length > 0,
|
|
607
|
+
`${name}: empty allowed-tools — inherits everything`,
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
const needed = opts.toolsNeeded ?? {};
|
|
611
|
+
// An exemption without a reason is not an exemption: an empty string would pass as "decided"
|
|
612
|
+
// while saying nothing. The same miss as "a counter that counts what it ignores".
|
|
613
|
+
for (const [tool, why] of Object.entries(needed))
|
|
614
|
+
assert.ok(
|
|
615
|
+
typeof why === "string" && why.trim().length >= 10,
|
|
616
|
+
`${name}: toolsNeeded.${tool} is declared without an intelligible reason. An exemption must ` +
|
|
617
|
+
`say WHY the tool is needed in substance — otherwise it is a silent disabling of the check.`,
|
|
618
|
+
);
|
|
619
|
+
for (const banned of ["Bash", "Agent", "Task"]) {
|
|
620
|
+
if (banned in needed) continue;
|
|
621
|
+
soft(() =>
|
|
622
|
+
assert.ok(
|
|
623
|
+
!c.declared.includes(banned),
|
|
624
|
+
`${name}: ${banned} appeared in the contract. It was dropped deliberately; bringing it back ` +
|
|
625
|
+
`restores a leg of the lethal trifecta (data + network + execution) on a skill that only ` +
|
|
626
|
+
`writes text. If the tool IS needed IN SUBSTANCE — declare it in toolsNeeded WITH A REASON, ` +
|
|
627
|
+
`not silently.`,
|
|
628
|
+
),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
// The other side: an exemption that exempts nothing is garbage, and garbage accumulates.
|
|
632
|
+
// A year later such lines read as "there was a risk here", though there is no risk.
|
|
633
|
+
for (const tool of Object.keys(needed))
|
|
634
|
+
soft(() =>
|
|
635
|
+
assert.ok(
|
|
636
|
+
c.declared.includes(tool),
|
|
637
|
+
`${name}: toolsNeeded.${tool} is declared, but ${tool} itself is NOT in the contract. ` +
|
|
638
|
+
`The exemption is stale — remove it, otherwise it describes a different skill.`,
|
|
639
|
+
),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// 2. Links. The class we got burned on 2026-08-27: a file moves one level deeper, the link
|
|
643
|
+
// stays as it was and silently stops resolving. A grep by name does not see that.
|
|
644
|
+
const dangling = danglingLinks(src, dir);
|
|
645
|
+
soft(() =>
|
|
646
|
+
assert.deepEqual(
|
|
647
|
+
dangling,
|
|
648
|
+
[],
|
|
649
|
+
`${name}: dangling links → ${dangling.join(", ")}. A skill naming a path that does not exist ` +
|
|
650
|
+
`is a dead instruction: the model reads it, does something adjacent and reports success.`,
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
// mutation: a planted broken link must be caught
|
|
654
|
+
assert.equal(
|
|
655
|
+
danglingLinks(src + "\n[x](./net-takogo-fayla.md)\n", dir).length,
|
|
656
|
+
dangling.length + 1,
|
|
657
|
+
`${name}: the link check did not fire on a planted broken link — it is dead`,
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// 3. Whether it is compiled. Editing the markdown by hand desynchronises the sha and gets
|
|
661
|
+
// quietly rolled back by the next `vigiles compile`.
|
|
662
|
+
soft(() =>
|
|
663
|
+
assert.ok(
|
|
664
|
+
/vigiles:sha256:/.test(src),
|
|
665
|
+
`${name}: no compilation mark. That means SKILL.md was edited by hand rather than through ` +
|
|
666
|
+
`.spec.ts — the next \`vigiles compile\` will overwrite the edit without warning.`,
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
// 4-5. Substantive anchors, each with BOTH halves.
|
|
671
|
+
for (const [re, why] of opts.mustSay || []) {
|
|
672
|
+
soft(() => assert.ok(re.test(src), `${name}: "${why}" is gone (${re})`));
|
|
673
|
+
// Mutation: without the matched fragment the predicate must fail. The replacement is GLOBAL —
|
|
674
|
+
// `replace` with a non-global regex removes only the first occurrence, and an anchor occurring
|
|
675
|
+
// twice (for example both in the description and in the body) would "pass" on the mutated text
|
|
676
|
+
// too. Then this assertion would fail rather than the anchor itself — that is, the message
|
|
677
|
+
// would point in the wrong direction.
|
|
678
|
+
const g = new RegExp(
|
|
679
|
+
re.source,
|
|
680
|
+
re.flags.includes("g") ? re.flags : re.flags + "g",
|
|
681
|
+
);
|
|
682
|
+
assert.equal(
|
|
683
|
+
re.test(src.replace(g, "")),
|
|
684
|
+
false,
|
|
685
|
+
`${name}: the anchor ${re} passes even on text WITHOUT it — the check pins nothing down`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
for (const [re, why] of opts.mustNotSay || []) {
|
|
689
|
+
soft(() => assert.ok(!re.test(src), `${name}: "${why}" came back (${re})`));
|
|
690
|
+
assert.equal(
|
|
691
|
+
re.test(src + "\n" + why + "\n"),
|
|
692
|
+
true,
|
|
693
|
+
`${name}: the ban ${re} does not fire on the planted line "${why}" — the check is dead`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (deferred.length)
|
|
698
|
+
throw new AggregateError(deferred, `${name}: ${deferred.length} findings`);
|
|
699
|
+
recordCheck();
|
|
700
|
+
return 1;
|
|
701
|
+
}
|