From f4325506d2911cf885a60aa73627bac237f3f18d Mon Sep 17 00:00:00 2001 From: Christopher Sarkissian Date: Sun, 13 Sep 2026 15:23:41 -0700 Subject: [PATCH 1/3] Remove customize overlay inject and exclude nested vendor trees from default coverage. Audit no longer depends on IDE hooks. Nested node_modules and dist no longer flood review-coverage. Co-authored-by: Cursor --- .cursor/hooks.json | 11 - .skeleton/customize/code-review.md | 111 ------ .skeleton/review-lock.json | 90 ++--- AGENTS.md | 2 +- README.md | 14 +- agent-suites/README.md | 3 +- .../fixtures/seeds/customize-demo.patch | 12 - agent-suites/skeleton-clean/scenarios.json | 15 - .../fixtures/seeds/customize-demo.patch | 50 --- agent-suites/skeleton-messy/scenarios.json | 15 - docs/developer/audit.md | 6 +- docs/developer/config.md | 8 +- docs/developer/customize.md | 78 ---- docs/developer/getting-started.md | 4 +- docs/developer/install.md | 2 +- docs/developer/troubleshooting.md | 17 +- docs/developer/validation.md | 2 +- package.json | 2 +- schemas/config.schema.json | 18 +- skeleton/SKILL.md | 27 +- src/__tests__/cli/hooks.test.ts | 95 ----- src/__tests__/cli/init.test.ts | 178 +-------- src/__tests__/cli/integration.test.ts | 13 - src/__tests__/cli/removed-commands.test.ts | 24 ++ src/audit/__tests__/collect.test.ts | 8 - .../.skeleton/customize/code-review.md | 3 - .../.skeleton/registry.md | 5 - src/audit/__tests__/review-coverage.test.ts | 36 +- src/audit/config/load.ts | 2 - src/audit/config/types.ts | 6 - src/audit/core/collect.ts | 3 +- src/audit/core/review-coverage.ts | 7 +- src/audit/policies/load.ts | 2 - src/cli.ts | 38 +- src/customize/__tests__/resolve.test.ts | 75 ---- src/customize/resolve.ts | 107 ------ src/hooks/customize-on-skill-read.ts | 7 - src/hooks/run.ts | 98 ----- src/init/init.ts | 30 +- src/init/merge-hooks.ts | 357 ------------------ src/init/merge-precommit.ts | 2 +- src/init/merge-scripts.ts | 32 ++ src/init/parse-args.ts | 10 +- src/init/resolve-hook-command.ts | 83 ---- .../claude-settings.fragment.json | 24 -- .../skeleton-init/codex-hooks.fragment.json | 15 - templates/skeleton-init/config.yaml | 5 - .../skeleton-init/cursor-hooks.fragment.json | 11 - templates/skeleton-init/skeleton.toml | 6 +- 49 files changed, 183 insertions(+), 1586 deletions(-) delete mode 100644 .cursor/hooks.json delete mode 100644 .skeleton/customize/code-review.md delete mode 100644 agent-suites/skeleton-clean/fixtures/seeds/customize-demo.patch delete mode 100644 agent-suites/skeleton-messy/fixtures/seeds/customize-demo.patch delete mode 100644 docs/developer/customize.md delete mode 100644 src/__tests__/cli/hooks.test.ts create mode 100644 src/__tests__/cli/removed-commands.test.ts delete mode 100644 src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/customize/code-review.md delete mode 100644 src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/registry.md delete mode 100644 src/customize/__tests__/resolve.test.ts delete mode 100644 src/customize/resolve.ts delete mode 100644 src/hooks/customize-on-skill-read.ts delete mode 100644 src/hooks/run.ts delete mode 100644 src/init/merge-hooks.ts create mode 100644 src/init/merge-scripts.ts delete mode 100644 src/init/resolve-hook-command.ts delete mode 100644 templates/skeleton-init/claude-settings.fragment.json delete mode 100644 templates/skeleton-init/codex-hooks.fragment.json delete mode 100644 templates/skeleton-init/cursor-hooks.fragment.json diff --git a/.cursor/hooks.json b/.cursor/hooks.json deleted file mode 100644 index 47af1a2..0000000 --- a/.cursor/hooks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "hooks": { - "postToolUse": [ - { - "command": "bun src/cli.ts hook customize", - "matcher": "Read" - } - ] - } -} diff --git a/.skeleton/customize/code-review.md b/.skeleton/customize/code-review.md deleted file mode 100644 index f7146ca..0000000 --- a/.skeleton/customize/code-review.md +++ /dev/null @@ -1,111 +0,0 @@ -# Code-review customize (skeleton) - - - - - - - -Injected on skill read. Prefer this overlay over portable thinned sections when both apply. Portable ledger / exit-gate rules still apply and must not be weakened. - -## Authoritative validation ladder - -Match [AGENTS.md](../../AGENTS.md) validation split: - -| Change type | Run before claiming validate / merge-ready | -| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| TypeScript under `src/` | `bun test` + `typecheck` + `build`. `validate:changed` fails uncovered coverage-candidate paths and audits owning papers | -| Docs / config (non-policy) | `bun run validate:changed -- ` or `bun run audit:self` | -| Plugin-wired policy YAML under `.skeleton/` | `bun run validate:changed -- ` (runs full docs and owned-skill prose) | -| Owned skill body (`SKILL.md` trees) | `bun run validate:changed -- ` (runs `audit skills`) or `bun run audit:skills` | -| Foreign / lockfile-synced skill body | skipped — lint in the owning skills/toolbox repo | - -`validate:changed` classifies code separately and leaves its correctness to native gates. It also discovers documents whose `review-deps` path or glob matched a changed file. A hash review-proof failure blocks until the document is re-read and explicitly attested. Code-only green is never code coverage. - -## Action bar (skeleton) - -Default filing remains merge-blockers only. - -- **Docs / tip / AGENTS wording is ship-blocker** only when it misroutes required validation or CI behavior (e.g. equates `audit self` with `audit skills` when coverage differs on excluded skill trees). -- Docs polish, catalog/SSOT nits, and test inventory without a reachable misroute → Noted or Deferred. -- Public-contract drift (runtime vs schema vs docs vs CLI tips) that can make consumers skip required gates → Action. - -For review-proof changes, inspect the document bytes, every `review-deps` dependency, `.skeleton/review-lock.json`, result schema, and explicit attestation CLI together. A mechanical date or lockfile update is an Action. - -## Review matrices (derive and check before theme closure) - -Close themes only after variant coverage for applicable rows -(`fix-loop-ledger.md` § Variant coverage before closure). - -### `validate:changed` routing - -| Dimension | Check | -| ---------------- | ------------------------------------------------------------------------------------------- | -| Input mixes | code with/without impacted docs, skill-only, policy-only, docs+policy, docs+skills, mixed inputs | -| Modes | local / pre-commit (no `--base`) vs CI `--base` | -| Fail posture | uncovered-changed-path, stage-required, orphan `.skeleton` YAML, fail-open coverage lies | -| Policy | plugin-wired vs unwired YAML; `config.yaml` not treated as policy | -| Equivalence tips | `audit docs` / `audit skills` / `audit self` only when they truly cover the same corpus | -| Review mode | date compatibility vs hash proof; changed doc bytes vs changed target bytes | - -Hotspots: `src/validate/changed.ts`, `AGENTS.md`, `docs/developer/validation.md`. - -### Skill-tree coverage - -| Dimension | Check | -| ---------------- | --------------------------------------------------------------------------------- | -| Trees | configured scan roots, `.agents`, `.claude`, other excluded skill dirs | -| Suites | path-scoped audit, bare `audit skills`, `audit self`, skills+prose-policy | -| Exclude behavior | `scan.exclude` vs deliberate include of excluded skill trees for skill-body prose | -| Policy prove | local and `--base` full docs + path-scoped skills prove | - -Hotspots: `src/audit/core/collect.ts`, `src/audit/core/context.ts`, `src/audit/core/skill-roots.ts`, `src/audit/run.ts`. - -### Plugin load / build - -| Dimension | Check | -| --------------- | -------------------------------------------------------------------------------------- | -| Exports | default vs named `rules` / `policies`; disagreement fail-closed | -| Build freshness | missing `.mjs`, mtime-only vs content fingerprint / stale source | -| Paths | relative, absolute CLI paths, normalization, traversal, symlink / realpath containment | -| Globs | `policies` globs under `.skeleton/`; orphan YAML loud fail | - -Hotspots: `src/plugins/load.ts`, `src/plugins/build.ts`, `src/plugins/paths.ts`. - -### Markdown / autofix rewrites - -| Dimension | Check | -| ---------- | --------------------------------------------------------------------------------- | -| Link forms | inline, titled (`](url "title")`), reference definitions, destination binding | -| Collision | duplicate URL text in label/title, prefix/suffix anchors, later duplicate targets | -| Surfaces | `.md`, `.mdc`, fenced code, inline code, skill-owned reference files | -| Offsets | parser spans vs whole-file `indexOf` / unsafe substring rewrite | - -Hotspots: `src/audit/core/markdown.ts`, `src/audit/fix/anchors.ts`, `src/audit/core/fix.ts`. - -### Public contract drift - -Inspect together when any one changes: runtime behavior, docs wording, schema, -exported types, CLI help/errors, tests, generated artifacts. - -## Contextual Full (skeleton) - -Append to pass-2+ member prompts with the portable ledger overlay: - -``` -Skeleton matrices: use .skeleton/customize/code-review.md rows for -validate routing, skill-tree coverage, plugin load/build, markdown/autofix, -and public-contract drift. Same invariant + new edge extends the existing -theme_id. Validate with the AGENTS.md ladder before exit language. -``` - -## Session handoff - -When fix-loop applies, include: stable-theme ledger, variants checked for -repeated themes, hotspot list, and which validation ladder commands ran. - -## Injection verification - -When this customization is loaded, include the exact line -`Customize loaded: SKELETON_CODE_REVIEW_CANARY_2026_07_14` -near the beginning of the review output. diff --git a/.skeleton/review-lock.json b/.skeleton/review-lock.json index f14e5fb..a1cc9a6 100644 --- a/.skeleton/review-lock.json +++ b/.skeleton/review-lock.json @@ -1,31 +1,20 @@ { "version": 2, "documents": { - ".skeleton/customize/code-review.md": { - "reviewedAt": "2026-09-13", - "documentHash": "sha256:61c3b86c898279893352dbdeafc6f9ac591730197d4614cb4d4cfc5b1ce0587b", - "reviewDependencies": { - "AGENTS.md": "sha256:db9e4e6756b85cd5882afda5e51ee676586af1dc621d2d4f111f93bc5820c0ba", - "docs/developer/validation.md": "sha256:278fed2d2b57985acbf555a532f4616ceda4c85c5e3b4f38f0433788642c1006", - "src/validate/changed.ts": "sha256:231ac839e2411a1826598a470a29a69f14387af3c6e0c115b73224b98500dd9a", - "src/validate/git-diff.ts": "sha256:b2c18f555d54d1bda79f3f246561640bb8ee3f5098980bbd8e16dfec283c2df4", - "src/validate/staged.ts": "sha256:50e512692be282c68f9568195c733e69d3b7c29eb9f1bec5f4bc12445e20aeb8" - } - }, "AGENTS.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:db9e4e6756b85cd5882afda5e51ee676586af1dc621d2d4f111f93bc5820c0ba", + "documentHash": "sha256:d80bd8595987f637e889ca2e9bd3e439c06cd7ed18afe59d295b57db53acd8e9", "reviewDependencies": { - "package.json": "sha256:0e72b7a7756de63dc8fa2994246129ae116a80188d23283e1b860f0812d58d5f", - "src/cli.ts": "sha256:1fc7a773f0295fd209a4d3baed536c24dc7bdf3d3c079118dad6f94a2de6026a" + "package.json": "sha256:2af98c1fe0094150ad0c43ae55703536e59057bc942f44cb1c50cf87a502fb30", + "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79" } }, "README.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:0dad5ac464bf617c00127ecafa1dbda459561f512030ec46ef22b7a48c5cf5b9", + "documentHash": "sha256:a7cc81e0e5a42deaf00c6c9826629d43ea7b981f7c496176748858bce353975e", "reviewDependencies": { - "package.json": "sha256:0e72b7a7756de63dc8fa2994246129ae116a80188d23283e1b860f0812d58d5f", - "src/cli.ts": "sha256:1fc7a773f0295fd209a4d3baed536c24dc7bdf3d3c079118dad6f94a2de6026a" + "package.json": "sha256:2af98c1fe0094150ad0c43ae55703536e59057bc942f44cb1c50cf87a502fb30", + "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79" } }, "docs/authoring.md": { @@ -37,11 +26,11 @@ }, "docs/developer/audit.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:d4cc4c04d85e1edf70c91925e1e6581892a6ebd1bf8188a14d1d694c445d6829", + "documentHash": "sha256:79dbe2165648a0a1bf808b677df375841b9f19cefbdbaf5dd64bd3c54b0e0360", "reviewDependencies": { - "src/audit/config/load.ts": "sha256:38b0f063408beb411872195f3f1e6183795d40b97380f5f5dec9169660556b1f", - "src/audit/config/types.ts": "sha256:5c23c9da56d56b818238c5669de30a1ca5cd2d02d2acd6e196810f6ae0cab1b0", - "src/audit/core/collect.ts": "sha256:853e7d16e005498e4040be12ef7f541c6b08a72ed104194dc941b58b9abdc8bf", + "src/audit/config/load.ts": "sha256:60b40e3b17f1912cd035956af963aad598161968e4c74507983a33dd013adc2a", + "src/audit/config/types.ts": "sha256:259ed258475e2e2eba40b3d329b7a633916a63bf84fa1d1f29e4158da77a78ba", + "src/audit/core/collect.ts": "sha256:3419d6a4df81fb32936e732283ae8e31eb27f0ba027256a6fa781376641ed3fe", "src/audit/core/context.ts": "sha256:c63e7a3eb7709a862e56c58044ed080446d19a1c2660a86bc1106e5723f90fc4", "src/audit/core/draft.ts": "sha256:68d5d980d34a439100960ce3b199f9f28c585ebdac7aae57a5da1f3f23002abb", "src/audit/core/fix.ts": "sha256:f5a2d134a70cfee721cb130bf38539000f66e053ab5a19967015718a22498287", @@ -49,7 +38,7 @@ "src/audit/core/markdown.ts": "sha256:fad91c2ed2fb18ef09faf1a2bad8b2e631dba30e1201bd57d9f8b508cf8183df", "src/audit/core/repo-files.ts": "sha256:9a008fa832ba85f6a9075347706f0c12756e6b12124d9a4df23ff60055c3bfc8", "src/audit/core/report.ts": "sha256:e6afa845a67ae3c48c3db3776c7ad599c55d665810f689fd6c021feaa4773ec3", - "src/audit/core/review-coverage.ts": "sha256:c0d1c619588b99aa17ef676e2e20d61fc5ef44d99b7f292d8c56b03880c4df26", + "src/audit/core/review-coverage.ts": "sha256:822546726ac5473cd1ec5827fecc3bf1b1956348d8d6fe9fe710489c6acf3156", "src/audit/core/review-deps.ts": "sha256:ebf37de0fe8a88a6ea3e75dec94d5e975e3ed55be3c55fff1130a41b297655ed", "src/audit/core/review-proof.ts": "sha256:cebc6edb3eb784335367df29125a9b5e6c2844836123655521a2b7afca47fe7b", "src/audit/core/shared.ts": "sha256:5d8ec137c0b2eb1bbe6041b09dba6c23fd825f946f241fb4616b790765825b11", @@ -63,7 +52,7 @@ "src/audit/fix/doc-meta.ts": "sha256:e5a9097605b5dd03b823fe3d16d3e7ddb178c95379cd05548c3a40f986de94ad", "src/audit/fix/match-anchor.ts": "sha256:d81ac6ae776b02402bedadc199425cbb3c9c593a7cda4271503479a61b153f4c", "src/audit/fix/ssot.ts": "sha256:69199c2e71eaabde0a0ce9d6d2209530f834e5323a6dbbd58edfe162f60fff6d", - "src/audit/policies/load.ts": "sha256:62a2cc8936bfce21045ad11e38b507314fe66702bb28d7edcfa93f70e7628f0e", + "src/audit/policies/load.ts": "sha256:c86aaf7184cc5519b31d85aa6d6bb20f2627974240b08443632d29dd76fe8310", "src/audit/policies/types.ts": "sha256:c1ee2f2b4fb7e4245b6f47031882d3dd122f31f48b06a09ea76e5b16e387ee73", "src/audit/rules/banned.ts": "sha256:0e201da5dc638fac994d47fd66223c93d6d839bdd4b4fc1a1478270b7cd8a71d", "src/audit/rules/doc-meta.ts": "sha256:680e6fd05b98a795c298c03ce9023eb72a2fcbeefce02edb203b618e662c3b43", @@ -80,25 +69,16 @@ "src/audit/rules/ssot-summary.ts": "sha256:853abc308ba1fbc2003069c66c41f5dcef7fb94f833a0ec50e715bd46d9031e5", "src/audit/rules/ssot.ts": "sha256:006ea9f05da7c3887fa7cddb82b54f931552689c78f4dd123614115984952007", "src/audit/run.ts": "sha256:01ee82a04363f23ad4d22b44d8773e7637fc08aecdfc54eb16d3feb8b177ead7", - "src/cli.ts": "sha256:1fc7a773f0295fd209a4d3baed536c24dc7bdf3d3c079118dad6f94a2de6026a", + "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79", "src/result-types.ts": "sha256:cfebe0bb52d5143ebee0e587d89bb1aac6cf8f035c92e4b5c9312cab607ea0a5" } }, "docs/developer/config.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:1e0797f91bb7a97a688f71016da93e9f5edfea1317299bde0157f22390b020ad", - "reviewDependencies": { - "src/audit/config/load.ts": "sha256:38b0f063408beb411872195f3f1e6183795d40b97380f5f5dec9169660556b1f", - "src/audit/config/types.ts": "sha256:5c23c9da56d56b818238c5669de30a1ca5cd2d02d2acd6e196810f6ae0cab1b0" - } - }, - "docs/developer/customize.md": { - "reviewedAt": "2026-09-13", - "documentHash": "sha256:2691ca04c5f1b94516c42643fbe1b6b3c9c8cb6476d7dc9241aa02d656a0b5b4", + "documentHash": "sha256:d55b96e68d3ca4842449f53a53d40f9b467cbd4a792e55a2c7ea669a62c9bbe2", "reviewDependencies": { - "src/customize/resolve.ts": "sha256:0bf0940f2b719ba07d80b282d1c005e1b50e34d78b73682d030b6e1a1d624bc4", - "src/hooks/customize-on-skill-read.ts": "sha256:7d6917be04c866b3a36b51d891a5e904fb571afcdaaef516e545bc364dd613c0", - "src/hooks/run.ts": "sha256:5bcd20491434abb6534c308bce0abc91cf9e6f5fb13640025ac9d868c038e1ef" + "src/audit/config/load.ts": "sha256:60b40e3b17f1912cd035956af963aad598161968e4c74507983a33dd013adc2a", + "src/audit/config/types.ts": "sha256:259ed258475e2e2eba40b3d329b7a633916a63bf84fa1d1f29e4158da77a78ba" } }, "docs/developer/doc-system.md": { @@ -112,28 +92,26 @@ }, "docs/developer/getting-started.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:92d444d464a6e3ea850a75ac4286d7bac30a6a0d876e45f2998860841283d933", + "documentHash": "sha256:e62fa3c2a9449ab21363019c4551ad0d34278cd8a9de13ba88a4408b3bae6111", "reviewDependencies": { - "src/cli.ts": "sha256:1fc7a773f0295fd209a4d3baed536c24dc7bdf3d3c079118dad6f94a2de6026a", - "src/init/init.ts": "sha256:a70cdf8a1a96a8438634cc69ce994eb84de763d4eb2f3feef947a1821c586109", - "src/init/merge-hooks.ts": "sha256:069088c4355dd52b7f225b5c88b2ae246c34fc3f206c9028c9d85f20a62c0362", - "src/init/merge-precommit.ts": "sha256:2fe8f93481571e7f56c0d380601488b36f8c9d12b2821f8dc8c324785daec2bf", + "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79", + "src/init/init.ts": "sha256:0980e3648bb9164d85de86d2aac8c5d945b38867e8fb64f0f05fd72d62025e00", + "src/init/merge-precommit.ts": "sha256:167604fe1a756612980887eb56a7dc534f4f4a6fa7cdf5e41cc3dafa8fd0f4d7", + "src/init/merge-scripts.ts": "sha256:521d29a0db6275c3b74dd26ebcc57eb4e46bd9ae21da3dd6a0fce8a42bdce3a0", "src/init/package-paths.ts": "sha256:0476e3c9067d9128da789d2c9741a00e91cb1c1e55cc014f9d38ecf6c7a527cf", - "src/init/parse-args.ts": "sha256:fa3fe7d67fdc04d95157af1cdc46d0ee9dda3f55a3d4c65a5ec95693117d1f2f", - "src/init/resolve-hook-command.ts": "sha256:dcad15b2281ad210d7d35230b8424af4b1fb7dbccee593e369d388c590ee71e1", + "src/init/parse-args.ts": "sha256:30c9b45ffefcd57957670e32333131dd472c6a935e5f4291ae0b1fdbfea10257", "src/init/skills-args.ts": "sha256:347a7e793d697a66adcdaf5f3183421191ee738949b5163284f44da7b30193ea" } }, "docs/developer/install.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:bb97167a3b784a7bf3da708ea046e0e090082cdd6a37bd2b6a4f83efd96564a8", + "documentHash": "sha256:bfba088b3cf9a7733dde57c833c74c0863e0cfe5b68ed252165fa2d588d49924", "reviewDependencies": { - "src/init/init.ts": "sha256:a70cdf8a1a96a8438634cc69ce994eb84de763d4eb2f3feef947a1821c586109", - "src/init/merge-hooks.ts": "sha256:069088c4355dd52b7f225b5c88b2ae246c34fc3f206c9028c9d85f20a62c0362", - "src/init/merge-precommit.ts": "sha256:2fe8f93481571e7f56c0d380601488b36f8c9d12b2821f8dc8c324785daec2bf", + "src/init/init.ts": "sha256:0980e3648bb9164d85de86d2aac8c5d945b38867e8fb64f0f05fd72d62025e00", + "src/init/merge-precommit.ts": "sha256:167604fe1a756612980887eb56a7dc534f4f4a6fa7cdf5e41cc3dafa8fd0f4d7", + "src/init/merge-scripts.ts": "sha256:521d29a0db6275c3b74dd26ebcc57eb4e46bd9ae21da3dd6a0fce8a42bdce3a0", "src/init/package-paths.ts": "sha256:0476e3c9067d9128da789d2c9741a00e91cb1c1e55cc014f9d38ecf6c7a527cf", - "src/init/parse-args.ts": "sha256:fa3fe7d67fdc04d95157af1cdc46d0ee9dda3f55a3d4c65a5ec95693117d1f2f", - "src/init/resolve-hook-command.ts": "sha256:dcad15b2281ad210d7d35230b8424af4b1fb7dbccee593e369d388c590ee71e1", + "src/init/parse-args.ts": "sha256:30c9b45ffefcd57957670e32333131dd472c6a935e5f4291ae0b1fdbfea10257", "src/init/skills-args.ts": "sha256:347a7e793d697a66adcdaf5f3183421191ee738949b5163284f44da7b30193ea" } }, @@ -149,7 +127,7 @@ }, "docs/developer/troubleshooting.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:69281df6dde433453c509710d141a7b61250993939a72f7cfc4e18e26486c193", + "documentHash": "sha256:039007b44df0f025b1b1c544402148e12069b4c7df0706c7786306d5f31a17d5", "reviewDependencies": { "src/audit/run.ts": "sha256:01ee82a04363f23ad4d22b44d8773e7637fc08aecdfc54eb16d3feb8b177ead7", "src/validate/changed.ts": "sha256:231ac839e2411a1826598a470a29a69f14387af3c6e0c115b73224b98500dd9a", @@ -159,7 +137,7 @@ }, "docs/developer/validation.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:278fed2d2b57985acbf555a532f4616ceda4c85c5e3b4f38f0433788642c1006", + "documentHash": "sha256:ce9da365562eaa63f860a96315c03d48755704f29b0f6c4ffb2c41bb6b98f41a", "reviewDependencies": { "src/validate/changed.ts": "sha256:231ac839e2411a1826598a470a29a69f14387af3c6e0c115b73224b98500dd9a", "src/validate/git-diff.ts": "sha256:b2c18f555d54d1bda79f3f246561640bb8ee3f5098980bbd8e16dfec283c2df4", @@ -170,16 +148,16 @@ "reviewedAt": "2026-09-13", "documentHash": "sha256:9cc412efac094e0e4194749866025ef315b9f19684314092058fc8827608d683", "reviewDependencies": { - "docs/developer/getting-started.md": "sha256:92d444d464a6e3ea850a75ac4286d7bac30a6a0d876e45f2998860841283d933", - "docs/developer/install.md": "sha256:bb97167a3b784a7bf3da708ea046e0e090082cdd6a37bd2b6a4f83efd96564a8", - "docs/developer/validation.md": "sha256:278fed2d2b57985acbf555a532f4616ceda4c85c5e3b4f38f0433788642c1006" + "docs/developer/getting-started.md": "sha256:e62fa3c2a9449ab21363019c4551ad0d34278cd8a9de13ba88a4408b3bae6111", + "docs/developer/install.md": "sha256:bfba088b3cf9a7733dde57c833c74c0863e0cfe5b68ed252165fa2d588d49924", + "docs/developer/validation.md": "sha256:ce9da365562eaa63f860a96315c03d48755704f29b0f6c4ffb2c41bb6b98f41a" } }, "skeleton/SKILL.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:016542ac69a5dc051c0e434a75f5accf842dbe87cbae93da227493fabb45fa3a", + "documentHash": "sha256:2e8dc324ab417532e3ac4905a0a08bc34528824ead8b4d196c80b90468b74d86", "reviewDependencies": { - "src/cli.ts": "sha256:1fc7a773f0295fd209a4d3baed536c24dc7bdf3d3c079118dad6f94a2de6026a" + "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79" } } } diff --git a/AGENTS.md b/AGENTS.md index 88e7cb5..b01a444 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ Never bump `last-reviewed` as a mechanical cleanup. After a complete re-read, at bun src/cli.ts audit docs --paths=docs/a.md --fix=doc-meta --confirm-reviewed ``` -Pre-commit: `.pre-commit-config.yaml` runs `bun src/cli.ts validate changed --staged`. Install [pre-commit](https://pre-commit.com/) once per machine, then `pre-commit install`. Customize IDE hooks from `skeleton init` are optional. +Pre-commit: `.pre-commit-config.yaml` runs `bun src/cli.ts validate changed --staged`. Install [pre-commit](https://pre-commit.com/) once per machine, then `pre-commit install`. Behavioral A/B dogfood (live Cursor, not part of `bun run check`): [agent-suites/README.md](agent-suites/README.md) · [refs/llm-harness.md](refs/llm-harness.md). diff --git a/README.md b/README.md index d332090..5250e34 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Agent repos get messy fast. Skills get copied around, docs disagree, links go st Skeleton is an SSOT linter for that layer. Define the contract once; Skeleton checks it locally and in CI. If a canonical doc disappears, SSOT markers drift, a skill index stops matching disk, or a local link breaks, the audit fails before merge. -Think ESLint — for the docs and skills your agents rely on. Primary CLI from `src/cli.ts`: `audit`, `validate`, `catalog`, `init`, and `build-plugin` (plus `customize` / `hook` when using overlays). Commands dispatch through that entry file. +Think ESLint — for the docs and skills your agents rely on. Primary CLI from `src/cli.ts`: `audit`, `validate`, `catalog`, `init`, and `build-plugin`. Commands dispatch through that entry file. Skeleton is **not** a runtime agent harness. It doesn't execute tools, enforce permissions, or manage memory. It checks whether the repo around those systems still holds together. @@ -78,7 +78,7 @@ npm install -D @csark0812/skeleton npx skeleton init --skills ``` -That writes `skeleton.toml`, ensures `.skeleton/customize/`, adds validation scripts, and may wire **optional** customize hooks for Cursor, Claude Code, and Codex. +That writes `skeleton.toml`, adds validation scripts, and writes `.pre-commit-config.yaml`. Edit `skeleton.toml` for your repo layout, then verify: @@ -119,9 +119,7 @@ skeleton.toml # scan perimeter, deny.paths, docsLint, … .skeleton/ ├── catalog.md # generated by `skeleton catalog` (gitignored) ├── review-lock.json # generated review evidence when reviewProof.mode = "hash" -├── plugins/ # optional consumer audit plugins (.ts + built .mjs) -└── customize/ # project-specific skill overrides (optional) - └── code-review.md +└── plugins/ # optional consumer audit plugins (.ts + built .mjs) ``` Legacy `.skeleton/config.yaml` still loads when no TOML is present. @@ -138,18 +136,17 @@ Then refresh the agent index: skeleton catalog ``` -Synced skills stay pristine. Project overrides live in `.skeleton/customize/.md` and inject when the skill is read — no editing copied `SKILL.md` files. +Edit synced skills in the owning toolbox repo. Consumer copies stay read-only. ## Commands ```bash -skeleton init [--skills] [--force-hooks] +skeleton init [--skills] skeleton catalog [--check] [--strict] skeleton audit docs|skills|self [--strict] [--json] [--paths=a,b] [--fix[=doc-meta|anchors|ssot]] [--dry-run] skeleton audit docs --paths=docs/a.md --fix=doc-meta --confirm-reviewed skeleton build-plugin [path] [--check] skeleton validate changed [--staged | --base ] [paths…] -skeleton customize resolve ``` **Validate changed** routes git diffs to the right audit: @@ -186,7 +183,6 @@ See [tiers](docs/tiers.md). Related work: [Toolbox](https://github.com/csark0812 - [Validation](docs/developer/validation.md) - [Audit rules](docs/developer/audit.md) - [Plugins](docs/developer/plugins.md) -- [Customize](docs/developer/customize.md) - [Authoring conventions](docs/authoring.md) ## Development diff --git a/agent-suites/README.md b/agent-suites/README.md index 889879d..7d24529 100644 --- a/agent-suites/README.md +++ b/agent-suites/README.md @@ -4,7 +4,7 @@ -These suites measure whether a clean Skeleton structure (registry, validation lanes, customize) improves **grounding**, **validation routing**, and **token efficiency** versus a messy control tree — not portable skill conformance (that lives in [toolbox](https://github.com/csark0812/toolbox) `agent-suites/`). +These suites measure whether a clean Skeleton structure (catalog, validation lanes) improves **grounding**, **validation routing**, and **token efficiency** versus a messy control tree — not portable skill conformance (that lives in [toolbox](https://github.com/csark0812/toolbox) `agent-suites/`). Committed stats and transcript excerpts: [`evidence/`](evidence/). Protocol SSOT: [`refs/llm-harness.md`](../refs/llm-harness.md). @@ -26,7 +26,6 @@ Paired scenario **names** match across clean/messy for `--compare-pairs skeleton | `grounding: conflicting docs` | SoT winner via registry | | `routing: docs-only change` | `validate:changed` lane | | `routing: owned skill body` | `audit:skills` lane | -| `customize: project binding` | `.skeleton/customize/` vs editing synced `SKILL.md` | ## Commands diff --git a/agent-suites/skeleton-clean/fixtures/seeds/customize-demo.patch b/agent-suites/skeleton-clean/fixtures/seeds/customize-demo.patch deleted file mode 100644 index e1036e8..0000000 --- a/agent-suites/skeleton-clean/fixtures/seeds/customize-demo.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/fixture-skills/demo-skill/SKILL.md b/fixture-skills/demo-skill/SKILL.md -new file mode 100644 ---- /dev/null -+++ b/fixture-skills/demo-skill/SKILL.md -@@ -0,0 +1,7 @@ -+# Demo skill -+ -+**Source of truth for** fixture demo-skill body (treat as synced / foreign — do not edit in consumers). -+ -+ -+ -+Protocol: answer with DEMO_SKILL_BASELINE when invoked with no customize overlay. diff --git a/agent-suites/skeleton-clean/scenarios.json b/agent-suites/skeleton-clean/scenarios.json index c5a7e91..76b2157 100644 --- a/agent-suites/skeleton-clean/scenarios.json +++ b/agent-suites/skeleton-clean/scenarios.json @@ -71,21 +71,6 @@ "The agent ran audit:skills (or bun run audit:skills) for the owned skill-body edit and did not treat path-scoped validate-changed alone as sufficient" ] } - }, - { - "name": "customize: project binding", - "prompt": "One-shot (no AskQuestion). A synced skill exists at `fixture-skills/demo-skill/SKILL.md`. Add a **project-specific** binding for this repo without editing that SKILL.md body.\n\nWrite `.skeleton/customize/demo-skill.md` that injects a canary line exactly: `SKELETON_DEMO_SKILL_CANARY_7f3a`.\n\nConfirm the file exists and quote the canary. Do not modify `fixture-skills/demo-skill/SKILL.md`.", - "seedPatch": "agent-suites/skeleton-clean/fixtures/seeds/customize-demo.patch", - "rubric": { - "must": [ - ".skeleton/customize/demo-skill.md", - "SKELETON_DEMO_SKILL_CANARY_7f3a" - ], - "mustNot": ["DEMO_SKILL_BASELINE edited", "audit all"], - "judge": [ - "The agent created a customize overlay under .skeleton/customize/demo-skill.md with the required canary and did not rewrite the synced SKILL.md as the project binding" - ] - } } ] } diff --git a/agent-suites/skeleton-messy/fixtures/seeds/customize-demo.patch b/agent-suites/skeleton-messy/fixtures/seeds/customize-demo.patch deleted file mode 100644 index c5a778d..0000000 --- a/agent-suites/skeleton-messy/fixtures/seeds/customize-demo.patch +++ /dev/null @@ -1,50 +0,0 @@ -diff --git a/fixture-skills/demo-skill/SKILL.md b/fixture-skills/demo-skill/SKILL.md -new file mode 100644 ---- /dev/null -+++ b/fixture-skills/demo-skill/SKILL.md -@@ -0,0 +1,7 @@ -+# Demo skill -+ -+**Source of truth for** fixture demo-skill body (treat as synced / foreign — do not edit in consumers). -+ -+ -+ -+Protocol: answer with DEMO_SKILL_BASELINE when invoked with no customize overlay. - ---- a/.skeleton/registry.md -+++ b/.skeleton/registry.md -@@ -1,30 +1,10 @@ - # Registry - -- -+ - --**Source of truth for** topic routing in this repo. Edit rows here; edit content in canonical files only. -+**Source of truth for** topic routing in this repo. - - ## Documentation - --| Topic | Canonical file | --| --------------------- | ---------------------------------------------------------- | --| Package overview | [README.md](../README.md) | --| Agent cold-start | [AGENTS.md](../AGENTS.md) | --| Authoring conventions | [authoring.md](../docs/authoring.md) | --| Ecosystem tiers | [tiers.md](../docs/tiers.md) | --| Getting started | [getting-started.md](../docs/developer/getting-started.md) | --| Install | [install.md](../docs/developer/install.md) | --| Config | [config.md](../docs/developer/config.md) | --| Validation | [validation.md](../docs/developer/validation.md) | --| Troubleshooting | [troubleshooting.md](../docs/developer/troubleshooting.md) | --| Doc system | [doc-system.md](../docs/developer/doc-system.md) | --| Audit | [audit.md](../docs/developer/audit.md) | --| Plugins | [plugins.md](../docs/developer/plugins.md) | --| Customize | [customize.md](../docs/developer/customize.md) | --| Skeleton skill | [SKILL.md](../skeleton/SKILL.md) | -- --## Customizations -- --| Topic | Canonical file | --| ------------------------------------------------------------------------------------------------------ | ------------------------------------------ | --| Customize: skeleton-specific code-review overlays (validation ladder, invariant matrices, Action bar). | [code-review.md](customize/code-review.md) | -+| Topic | Canonical file | -+| ----- | -------------- | diff --git a/agent-suites/skeleton-messy/scenarios.json b/agent-suites/skeleton-messy/scenarios.json index a5a8156..9fed173 100644 --- a/agent-suites/skeleton-messy/scenarios.json +++ b/agent-suites/skeleton-messy/scenarios.json @@ -70,21 +70,6 @@ "The agent ran audit:skills (or bun run audit:skills) for the owned skill-body edit and did not treat path-scoped validate-changed alone as sufficient" ] } - }, - { - "name": "customize: project binding", - "prompt": "One-shot (no AskQuestion). A synced skill exists at `fixture-skills/demo-skill/SKILL.md`. Add a **project-specific** binding for this repo without editing that SKILL.md body.\n\nWrite `.skeleton/customize/demo-skill.md` that injects a canary line exactly: `SKELETON_DEMO_SKILL_CANARY_7f3a`.\n\nConfirm the file exists and quote the canary. Do not modify `fixture-skills/demo-skill/SKILL.md`.", - "seedPatch": "agent-suites/skeleton-messy/fixtures/seeds/customize-demo.patch", - "rubric": { - "must": [ - ".skeleton/customize/demo-skill.md", - "SKELETON_DEMO_SKILL_CANARY_7f3a" - ], - "mustNot": ["DEMO_SKILL_BASELINE edited", "audit all"], - "judge": [ - "The agent created a customize overlay under .skeleton/customize/demo-skill.md with the required canary and did not rewrite the synced SKILL.md as the project binding" - ] - } } ] } diff --git a/docs/developer/audit.md b/docs/developer/audit.md index 93488d5..533c131 100644 --- a/docs/developer/audit.md +++ b/docs/developer/audit.md @@ -18,7 +18,7 @@ skeleton audit self # config + all rules (scan corpus; excluded owned skill `review-deps` is an opt-in dependency graph from documents to exact repo-relative paths or globs. `validate changed` uses it for any changed file type; hash review proof invalidates the document when a resolved dependency byte or set changes. Local `audit docs` and `audit self` write `.skeleton/catalog.md` on each run. The write is skipped when `CI=true`. -CLI dispatch in `src/cli.ts` covers `audit`, `build-plugin`, `catalog`, `customize`, `hook`, `init`, `register` (removed — errors with migration text), and `validate`. The audit runner exports `runAudit`, `parseAuditArgs`, and `AuditCliOptions` for suites `docs`, `skills`, and `self`. +CLI dispatch in `src/cli.ts` covers `audit`, `build-plugin`, `catalog`, `init`, and `validate`. `register`, `customize`, and `hook` error with migration text. The audit runner exports `runAudit`, `parseAuditArgs`, and `AuditCliOptions` for suites `docs`, `skills`, and `self`. Autofix (docs only): @@ -59,7 +59,7 @@ Lint skill **bodies** where they are authored: | Repo role | What to audit | | --------------------- | ------------------------------------------------------------------------------------ | | Skills / toolbox repo | All (or owned) `SKILL.md` trees via `audit skills` | -| Consumer app repo | `.skeleton/customize/**`, config/policies; skip foreign synced skill bodies | +| Consumer app repo | config/policies; skip foreign synced skill bodies | Classification (defaults work with no config): @@ -68,7 +68,7 @@ Classification (defaults work with no config): 3. `skills-lock.json` entry with `sourceType` other than `local` (e.g. `github`) → foreign 4. Otherwise → owned -Foreign skills remain discoverable for link resolution and customize inject, but are omitted from docs/self/skills corpora, doc-meta scope, and CI policy skill proves. +Foreign skills remain discoverable for link resolution, but are omitted from docs/self/skills corpora, doc-meta scope, and CI policy skill proves. ### `scan.nonPublicSkills` diff --git a/docs/developer/config.md b/docs/developer/config.md index 066bcec..c1674ce 100644 --- a/docs/developer/config.md +++ b/docs/developer/config.md @@ -8,7 +8,7 @@ Machine schema: [`schemas/config.schema.json`](../../schemas/config.schema.json) (validates the loaded object). Init template: `templates/skeleton-init/skeleton.toml`. Day-one walkthrough: [getting started](getting-started.md). -Loader: `loadConfig` / `loadConfigDetailed` / `findRepoRoot` / `mergedExcludes` in `src/audit/config/load.ts`. Typed shape: `SkeletonConfig` (`ScanConfig`, `DocsLintConfig`, `DenyConfig`, `SkillOwnershipConfig`, `ReviewProofConfig`, `ReviewCoverageConfig`, `CustomizeConfig`). +Loader: `loadConfig` / `loadConfigDetailed` / `findRepoRoot` / `mergedExcludes` in `src/audit/config/load.ts`. Typed shape: `SkeletonConfig` (`ScanConfig`, `DocsLintConfig`, `DenyConfig`, `SkillOwnershipConfig`, `ReviewProofConfig`, `ReviewCoverageConfig`). Preferred path: **`skeleton.toml` at the repo root**. Legacy `.skeleton/config.yaml` still loads when no TOML is present. If both exist, TOML wins and the CLI warns that YAML is ignored. @@ -30,7 +30,6 @@ Top-level required keys: `scan` and `daysUntilStale`. Inside `scan`, required: ` | `scan.nonPublicSkills` | Slugs on disk that must **not** appear in nested skills README `## Taxonomy` | | `plugins` | Plugin entry paths relative to `.skeleton/` (each needs a built sibling `.mjs`) — [plugins](plugins.md) | | `draftPathPrefixes` | Allow-list prefixes for draft-marker prose policy (plus `_draft-*.md`). Not `scan.exclude` | -| `customize.alwaysInclude` | Basenames under `.skeleton/customize/` appended on every skill inject — [customize](customize.md) | | `skillOwnership` | Provenance-aware skill body linting (see below) | | `reviewProof` | Hash-backed evidence for exact reviewed document and `review-deps` bytes (see below) | | `reviewCoverage` | Globs that must appear in at least one scanned paper's `review-deps` (see below) | @@ -84,11 +83,11 @@ exclude = ["src/**/__tests__/**", "src/**/*.test.ts"] Files that match `include` (minus `exclude` and built-in test or fixture excludes) must appear in at least one scanned paper's `review-deps`. The global `review-coverage` rule checks the whole set. `validate changed` also fails `uncovered-changed-path` when a changed candidate has no owner. -Omit the section to use built-in code defaults (`**/*.{ts,tsx,js,jsx,mjs,cjs,py}`, `package.json`, `project.json`). Built-in excludes drop tests, fixtures, templates, and `.skeleton/plugins/**`. Production `.mjs` stays in the coverage set. Set `include = []` to disable the gate. +Omit the section to use built-in code defaults (`**/*.{ts,tsx,js,jsx,mjs,cjs,py}`, `package.json`, `project.json`). Built-in excludes drop tests, fixtures, templates, nested `node_modules` / `dist` / `.venv`, and `.skeleton/plugins/**`. Production `.mjs` stays in the coverage set. Set `include = []` to disable the gate. ## `skillOwnership` -When a consumer repo syncs skills from a toolbox (via `skills-lock.json`), Skeleton skips foreign skill **bodies** so linting stays with the owning repo. That skip covers skill-body lint (`audit skills`), path-scoped validate routing, and **doc-meta** for paths under foreign skill trees — including SSOT-bearing `references/**` files. Consumer customizations under `.skeleton/customize/` remain audited here. +When a consumer repo syncs skills from a toolbox (via `skills-lock.json`), Skeleton skips foreign skill **bodies** so linting stays with the owning repo. That skip covers skill-body lint (`audit skills`), path-scoped validate routing, and **doc-meta** for paths under foreign skill trees — including SSOT-bearing `references/**` files. | Key | Purpose | | -------------- | ----------------------------------------------------------------------- | @@ -117,7 +116,6 @@ See [audit](audit.md#skill-ownership-consumer-vs-toolbox) and [validation](valid | Path-scoped `validate changed` | Files in `scan.include` (minus exclude) get docs audit; owned skill trees run the skills suite; foreign lock skills skip; coverage-candidate code without an owning paper fails | | Global rules (`--base` / full audit) | `deny.paths` (rule `banned`), SSOT dual/malformed, near-dupe, ssot-summary, coverage outside include, review-coverage, scan-roots, skill-index | | Prose policies | Idle until `plugins` contribute policy YAML | -| Customize inject | `customize.alwaysInclude` (optional hooks / `customize resolve`); customize paths are always in the audit corpus | | Skill body ownership | `skillOwnership` + `skills-lock.json` (foreign bodies skipped) | | Catalog | Local audit and `validate changed` write `.skeleton/catalog.md` on each run; skipped when `CI=true` | diff --git a/docs/developer/customize.md b/docs/developer/customize.md deleted file mode 100644 index 91d49b4..0000000 --- a/docs/developer/customize.md +++ /dev/null @@ -1,78 +0,0 @@ -# Customize - - - - - - - -Resolve overlays with `resolveCustomize` / `resolveCustomizeFromRoot` (`CUSTOMIZE_PREFIX`). Hook entry: `runCustomizeHook`. - -Hook inject failures: [troubleshooting](troubleshooting.md). `customize.alwaysInclude` key: [config](config.md). Customize IDE hooks are **optional** — audit/validate/catalog work without them. - -## Layout - -``` -.skeleton/customize/.md -``` - -`skeleton init` wires IDE hooks to a cwd-local -`node node_modules/@csark0812/skeleton/dist/cli.js hook customize` (so runners that -lack `node_modules/.bin` on `PATH` still work). Inside this repo the hook runs -`bun src/cli.ts hook customize`. -Customize Markdown is always included in skeleton's audit corpus; consumers do -not add `.skeleton/customize/**` to `scan.include`. - -## Inject gates - -| Triggers inject | Does not inject | -| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -| `Read` / `read_file` of a path ending in `/SKILL.md` | `Grep`, shell `cat`/`head` | -| `Read` / `read_file` under a skill tree (`.claude/skills//**`, `.agents/skills//**`, or flat `/references/**` when that skill exists) | Non-skill paths (no resolvable slug) | -| Claude `Skill` tool (slug from tool input) | Missing/unresolvable local package CLI (hooks no-op / fail) | - -### Host matchers (init templates) - -| Host | Matcher(s) | -| ----------- | ---------------------------------------- | -| Cursor | `Read` only (no Claude-style Skill tool) | -| Claude Code | `Read` + `Skill` | -| Codex | `read_file` only | - -## `alwaysInclude` - -In `skeleton.toml` (or legacy `.skeleton/config.yaml`): - -```toml -[customize] -alwaysInclude = ["shared-agent-references.md"] -``` - -On every inject for slug `X`, the hook and `skeleton customize resolve X` concatenate: - -1. `.skeleton/customize/X.md` (if present and non-empty) -2. Each listed basename under `.skeleton/customize/` (skip if it is the same file as `X.md`) - -If the slug file is missing or empty but `alwaysInclude` resolves content, inject still fires. - -Recommended for toolbox consumers: put the consumer `references/*` → docs remap table in `shared-agent-references.md` and list it under `alwaysInclude` so agents do not need a second hop. - -## Manual resolve - -```bash -skeleton customize resolve code-review -skeleton customize resolve code-review --json -# or, from a consumer repo: bunx skeleton customize resolve code-review -``` - -There is no `bun run skeleton customize …` script by default — use the `skeleton` bin from `@csark0812/skeleton`. - -## Optional catalog membership - -Customize files may include a `source-of-truth` marker if you want them in `skeleton catalog`. Hooks remain optional — `skeleton customize resolve` works without IDE inject. - -Do not edit synced toolbox `SKILL.md` files — override in `.skeleton/customize/`. - -Synced foreign skill bodies (declared in `skills-lock.json` with non-`local` provenance) are skipped by consumer `audit skills` / validate routing; the owning toolbox repo runs skill-body lint. Customize overlays and config stay in this repo's audit corpus — see [config](config.md#skillownership). - -If resolve works but IDE inject does not, see [troubleshooting](troubleshooting.md#customize-hook-not-injecting). diff --git a/docs/developer/getting-started.md b/docs/developer/getting-started.md index 13ddd9c..4ef07f4 100644 --- a/docs/developer/getting-started.md +++ b/docs/developer/getting-started.md @@ -17,7 +17,7 @@ npm install -D @csark0812/skeleton npx skeleton init --skills ``` -Init writes `skeleton.toml`, ensures `.skeleton/customize/`, writes `.pre-commit-config.yaml`, may merge **optional** IDE customize hooks, and adds `validate:changed` / `validate:ci` scripts to `package.json`. Hash review proof is on by default. IDE customize hooks are not required for audit. +Init writes `skeleton.toml`, writes `.pre-commit-config.yaml`, and adds `validate:changed` / `validate:ci` scripts to `package.json`. Hash review proof is on by default. ## 2. Set the scan perimeter @@ -139,7 +139,6 @@ Details: [install](install.md). - [ ] `npx skeleton catalog` - [ ] `npx skeleton audit docs` - [ ] `pre-commit install` -- [ ] (Optional) IDE customize hooks ## Next @@ -148,6 +147,5 @@ Details: [install](install.md). | Config keys and examples | [config](config.md) | | What to run after a change | [validation](validation.md) | | SSOT / catalog / doc-meta | [doc system](doc-system.md) | -| Skill overrides without editing synced skills | [customize](customize.md) | | Product-specific audit rules | [plugins](plugins.md) | | Common failures | [troubleshooting](troubleshooting.md) | diff --git a/docs/developer/install.md b/docs/developer/install.md index 39a1da6..fd62505 100644 --- a/docs/developer/install.md +++ b/docs/developer/install.md @@ -17,7 +17,7 @@ npx skeleton init --skills `--skills` runs `npx skills add csark0812/skeleton …` with sensible defaults (`--skill skeleton`, `-a cursor claude-code codex`, `-y`). Pass any [skills add flags](https://github.com/vercel-labs/skills) after `--skills` — e.g. `-g` / `--global`, `--all`, `-a codex`, `--copy`, `--list`. -Init writes `skeleton.toml` / `.skeleton/`, writes `.pre-commit-config.yaml`, may merge **optional** IDE customize hooks, and adds `validate:changed` / `validate:ci` scripts. +Init writes `skeleton.toml` / `.skeleton/`, writes `.pre-commit-config.yaml`, and adds `validate:changed` / `validate:ci` scripts. ## Config diff --git a/docs/developer/troubleshooting.md b/docs/developer/troubleshooting.md index ed74c93..fcf9e7e 100644 --- a/docs/developer/troubleshooting.md +++ b/docs/developer/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting - + @@ -68,21 +68,6 @@ skeleton validate changed --staged skeleton validate changed --base origin/main ``` -## Customize hook not injecting - -**Checklist:** - -1. Package installed: `node_modules/@csark0812/skeleton` present (or linked in monorepos). Init writes a cwd-local `node …/dist/cli.js hook customize` command, so `PATH` / `node_modules/.bin` is not required. -2. Init hooks present: `.cursor/hooks.json`, `.claude/settings.json`, and/or `.codex/hooks.json` contain a skeleton `hook customize` command (or the legacy `customize-on-skill-read` entrypoint). -3. Host matcher matches the tool: Cursor `Read`, Claude `Read`/`Skill`, Codex `read_file`. Grep/shell never inject. -4. Slug resolve works: - -```bash -skeleton customize resolve -``` - -If resolve prints content but the IDE still skips inject, re-run `npx skeleton init` (or `--force-hooks` if you intentionally overrode the skeleton-owned hook fields). Details: [customize](customize.md). - ## Plugin `.mjs` missing or stale **Cause:** Runtime loads only the sibling `.mjs`; authors must commit source **and** build artifact. diff --git a/docs/developer/validation.md b/docs/developer/validation.md index e7921b7..ff2aa0c 100644 --- a/docs/developer/validation.md +++ b/docs/developer/validation.md @@ -71,7 +71,7 @@ Skill bodies are not path-scoped on the docs lane. **Foreign** skills (`skills-lock.json` entries with `sourceType` other than `local`, e.g. `github`) are skipped so consumer repos don't double-lint synced toolbox copies — including doc-meta on SSOT-bearing skill `references/**` paths. Override with `skillOwnership.ownedSlugs` / `foreignSlugs` — see [config](config.md#skillownership). -`audit self` covers the scan corpus; excluded owned skill trees still need `audit skills`. Customize overlays under `.skeleton/customize/` stay in the consumer audit corpus. +`audit self` covers the scan corpus; excluded owned skill trees still need `audit skills`. ### Plugin policy YAML diff --git a/package.json b/package.json index e6788e9..d8ffbac 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "packageManager": "bun@1.2.21", "scripts": { - "build": "bun build src/cli.ts --target=node --outfile=dist/cli.js && bun build src/hooks/customize-on-skill-read.ts --target=node --outfile=dist/hooks/customize-on-skill-read.js && bun build src/plugin-types.ts --target=node --format=esm --outfile=dist/plugin-types.js --packages=external && bun build src/result-types.ts --target=node --format=esm --outfile=dist/result-types.js --packages=external && tsc -p tsconfig.types.json", + "build": "bun build src/cli.ts --target=node --outfile=dist/cli.js && bun build src/plugin-types.ts --target=node --format=esm --outfile=dist/plugin-types.js --packages=external && bun build src/result-types.ts --target=node --format=esm --outfile=dist/result-types.js --packages=external && tsc -p tsconfig.types.json", "test": "bun test", "test:audit": "bun test src/audit/__tests__", "test:cli": "bun test src/__tests__/cli", diff --git a/schemas/config.schema.json b/schemas/config.schema.json index 997a5f2..f4d81c6 100644 --- a/schemas/config.schema.json +++ b/schemas/config.schema.json @@ -88,26 +88,10 @@ } } }, - "customize": { - "type": "object", - "additionalProperties": false, - "description": "Customize inject options for IDE hooks and `skeleton customize resolve`", - "properties": { - "alwaysInclude": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "pattern": "^[a-zA-Z0-9._-]+\\.md$" - }, - "description": "Basenames under .skeleton/customize/ appended whenever a skill customize injects (even if the slug file is missing or empty)" - } - } - }, "skillOwnership": { "type": "object", "additionalProperties": false, - "description": "Provenance-aware skill body linting: foreign (synced) skills are skipped; owned local skills and .skeleton/customize stay audited", + "description": "Provenance-aware skill body linting: foreign (synced) skills are skipped; owned local skills stay audited", "properties": { "lockfile": { "type": "string", diff --git a/skeleton/SKILL.md b/skeleton/SKILL.md index efbcbdc..fc8c170 100644 --- a/skeleton/SKILL.md +++ b/skeleton/SKILL.md @@ -1,6 +1,6 @@ --- name: skeleton -description: Agent ops manual for skeleton-enabled repos — init, catalog, audit, optional customize hooks, and toolbox skill overrides. Use when editing skeleton.toml / .skeleton/, syncing toolbox skills, or running skeleton CLI. +description: Agent ops manual for skeleton-enabled repos — init, catalog, audit, and toolbox skill ownership. Use when editing skeleton.toml / .skeleton/, syncing toolbox skills, or running skeleton CLI. --- @@ -24,7 +24,7 @@ Human docs: [getting started](https://github.com/csark0812/skeleton/blob/main/do ## When to use -- Edit `skeleton.toml` or `.skeleton/customize/.md` +- Edit `skeleton.toml` or `.skeleton/` - Run `skeleton audit`, `skeleton validate`, or `skeleton catalog` - Sync or update skills from an external toolbox repo - Avoid editing synced toolbox skill copies in the consumer repo @@ -33,7 +33,7 @@ Catalog honesty is enforced by `audit docs` (`ssot-summary` / near-dupe) — do Doc-meta: one authored `last-reviewed` (human claim). Git is last-edit — no parallel edit stamp. When hash review proof is enabled, `.skeleton/review-lock.json` binds that claim to the exact document and `review-deps` bytes. `daysUntilStale` remains an optional re-read cadence. Details: [doc system](https://github.com/csark0812/skeleton/blob/main/docs/developer/doc-system.md#doc-meta). -Not for: normal feature work that only reads toolbox skills (optional customize hooks can inject on skill reads). +Not for: normal feature work that only reads toolbox skills. ## Layout @@ -42,27 +42,11 @@ skeleton.toml # preferred root config (scan, stale, docsLint) .skeleton/ ├── catalog.md # generated, gitignored — local audit writes it ├── review-lock.json # review hashes when reviewProof.mode = "hash" -├── plugins/ # optional audit plugins (.ts + .mjs) -└── customize/ # per-slug overrides for toolbox-bound skills - └── .md +└── plugins/ # optional audit plugins (.ts + .mjs) ``` Legacy `.skeleton/config.yaml` still loads if no `skeleton.toml` is present. -## Customize hooks (optional) - -Hooks are an optional improvement, not required for audit/validate/catalog. - -`skeleton init` may merge IDE hooks that run a cwd-local -`node node_modules/@csark0812/skeleton/dist/cli.js hook customize` on skill reads. -Inside this repo the hook runs `bun src/cli.ts hook customize`. - -- Hook injects `.skeleton/customize/.md` when path is `/SKILL.md` **or** under a skill tree -- **Never edit synced toolbox `SKILL.md` files in the consumer repo** — override in `.skeleton/customize/.md` -- Manual fallback: `skeleton customize resolve ` - -Details: [docs/developer/customize.md](https://github.com/csark0812/skeleton/blob/main/docs/developer/customize.md) - ## Setup ```bash @@ -101,9 +85,8 @@ Do not run this command as a mechanical date cleanup. Bare `--fix` changes ancho | `skeleton validate changed` | Changed-file validation + dependency-driven doc discovery | | `skeleton validate changed --staged` | Pre-commit hook (index bytes + coverage + owning papers) | | `skeleton validate changed --base origin/main` | CI / PR | -| `skeleton customize resolve ` | Print merged customize for a skill slug | -`register` was removed — add a source-of-truth marker and run `skeleton catalog`. +`register`, `customize`, and `hook` were removed. Add a source-of-truth marker and run `skeleton catalog`. Edit skills in the owning repo. Plugins: [docs/developer/plugins.md](https://github.com/csark0812/skeleton/blob/main/docs/developer/plugins.md) diff --git a/src/__tests__/cli/hooks.test.ts b/src/__tests__/cli/hooks.test.ts deleted file mode 100644 index 3b2a026..0000000 --- a/src/__tests__/cli/hooks.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { spawnSync } from "node:child_process"; -import { join } from "node:path"; - -const HOOK = join(import.meta.dir, "../../hooks/customize-on-skill-read.ts"); -const CLI = join(import.meta.dir, "../../cli.ts"); -const NESTED_SKILLS_CUSTOMIZE = join( - import.meta.dir, - "../../audit/__tests__/fixtures/nested-skills-customize", -); - -// The standalone entrypoint and `skeleton hook customize` share one implementation, -// so exercise both to guarantee parity. -const ENTRYPOINTS: Array<{ name: string; argv: string[] }> = [ - { name: "customize-on-skill-read hook", argv: [HOOK] }, - { name: "skeleton hook customize", argv: [CLI, "hook", "customize"] }, -]; - -function runHook(argv: string[], stdin: string): { stdout: string; exitCode: number | null } { - const proc = spawnSync("bun", argv, { - cwd: NESTED_SKILLS_CUSTOMIZE, - input: stdin, - encoding: "utf8", - }); - return { stdout: proc.stdout, exitCode: proc.status }; -} - -describe.each(ENTRYPOINTS)("$name", ({ argv }) => { - const runHookEntry = (stdin: string) => runHook(argv, stdin); - - it("returns noop for non-skill Read", () => { - const { stdout, exitCode } = runHookEntry( - JSON.stringify({ - tool_name: "Read", - tool_input: { path: "/repo/docs/README.md" }, - }), - ); - expect(exitCode).toBe(0); - expect(stdout.trim()).toBe("{}"); - }); - - it("injects customize content for skill-tree references Read", () => { - const refPath = join( - NESTED_SKILLS_CUSTOMIZE, - ".claude/skills/code-review/references/planning/build.md", - ); - const { stdout, exitCode } = runHookEntry( - JSON.stringify({ - tool_name: "Read", - hook_event_name: "postToolUse", - tool_input: { path: refPath }, - }), - ); - expect(exitCode).toBe(0); - const parsed = JSON.parse(stdout); - expect(parsed.additional_context).toContain("Code review customize"); - }); - - it("injects customize content for SKILL.md Read (Cursor)", () => { - const skillPath = join(NESTED_SKILLS_CUSTOMIZE, ".claude/skills/code-review/SKILL.md"); - const { stdout, exitCode } = runHookEntry( - JSON.stringify({ - tool_name: "Read", - hook_event_name: "postToolUse", - tool_input: { path: skillPath }, - }), - ); - expect(exitCode).toBe(0); - const parsed = JSON.parse(stdout); - expect(parsed.additional_context).toContain("Code review customize"); - }); - - it("injects customize content for Skill tool (Claude)", () => { - const { stdout, exitCode } = runHookEntry( - JSON.stringify({ - tool_name: "Skill", - tool_input: { skill: "code-review" }, - }), - ); - expect(exitCode).toBe(0); - const parsed = JSON.parse(stdout); - expect(parsed.hookSpecificOutput.additionalContext).toContain("Code review customize"); - }); - - it("returns noop when no customize override", () => { - const { stdout, exitCode } = runHookEntry( - JSON.stringify({ - tool_name: "Skill", - tool_input: { skill: "missing" }, - }), - ); - expect(exitCode).toBe(0); - expect(stdout.trim()).toBe("{}"); - }); -}); diff --git a/src/__tests__/cli/init.test.ts b/src/__tests__/cli/init.test.ts index ba09311..4fcd782 100644 --- a/src/__tests__/cli/init.test.ts +++ b/src/__tests__/cli/init.test.ts @@ -1,30 +1,13 @@ import { afterEach, describe, expect, it } from "bun:test"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runInit, skillsAddArgs } from "../../init/init.ts"; -import { mergeHookConfigs, mergePackageJsonScripts } from "../../init/merge-hooks.ts"; +import { mergePackageJsonScripts } from "../../init/merge-scripts.ts"; import { parseInitArgs } from "../../init/parse-args.ts"; -import { isSkeletonHookCommand, resolveHookCommand } from "../../init/resolve-hook-command.ts"; let tempDirs: string[] = []; -/** Cursor's default sandbox blocks mkdir of `.cursor` / `.claude` even under os.tmpdir(). */ -function canWriteAgentDirs(): boolean { - const probe = mkdtempSync(join(tmpdir(), "skeleton-agent-dir-probe-")); - try { - mkdirSync(join(probe, ".cursor"), { recursive: true }); - mkdirSync(join(probe, ".claude"), { recursive: true }); - return true; - } catch { - return false; - } finally { - rmSync(probe, { recursive: true, force: true }); - } -} - -const describeHooks = canWriteAgentDirs() ? describe : describe.skip; - function makeRepo(extra: Record = {}): string { const dir = mkdtempSync(join(tmpdir(), "skeleton-init-")); tempDirs.push(dir); @@ -43,46 +26,6 @@ afterEach(() => { }); describe("skeleton init helpers", () => { - it("emits a cwd-local node cli hook when the package is installed", () => { - const cwd = makeRepo(); - mkdirSync(join(cwd, "node_modules/@csark0812/skeleton/dist"), { recursive: true }); - writeFileSync(join(cwd, "node_modules/@csark0812/skeleton/dist/cli.js"), "// stub\n"); - writeFileSync( - join(cwd, "node_modules/@csark0812/skeleton/package.json"), - JSON.stringify({ name: "@csark0812/skeleton", type: "module" }), - ); - const command = resolveHookCommand(cwd); - expect(command).toBe("node node_modules/@csark0812/skeleton/dist/cli.js hook customize"); - }); - - it("falls back to a cwd-local node path when the package is missing", () => { - const cwd = mkdtempSync(join(tmpdir(), "skeleton-init-fallback-")); - tempDirs.push(cwd); - const command = resolveHookCommand(cwd); - expect(command).toBe("node node_modules/@csark0812/skeleton/dist/cli.js hook customize"); - }); - - it("resolveHookCommand returns a detectable skeleton customize hook", () => { - const cwd = makeRepo(); - const command = resolveHookCommand(cwd); - expect(isSkeletonHookCommand(command)).toBe(true); - expect(command.includes("hook customize")).toBe(true); - }); - - it("detects both the CLI form and the legacy standalone hook", () => { - expect(isSkeletonHookCommand("skeleton hook customize")).toBe(true); - expect(isSkeletonHookCommand("bun src/cli.ts hook customize")).toBe(true); - expect( - isSkeletonHookCommand("node node_modules/@csark0812/skeleton/dist/cli.js hook customize"), - ).toBe(true); - expect( - isSkeletonHookCommand( - "node node_modules/@csark0812/skeleton/dist/hooks/customize-on-skill-read.js", - ), - ).toBe(true); - expect(isSkeletonHookCommand("echo user-hook")).toBe(false); - }); - it("mergePackageJsonScripts is skipped without package.json", () => { const cwd = mkdtempSync(join(tmpdir(), "skeleton-init-nopkg-")); tempDirs.push(cwd); @@ -90,26 +33,28 @@ describe("skeleton init helpers", () => { }); it("parseInitArgs forwards unknown flags to skills", () => { - expect(parseInitArgs(["--force-hooks", "--skills", "-g", "--all"])).toEqual({ - forceHooks: true, + expect(parseInitArgs(["--skills", "-g", "--all"])).toEqual({ skills: true, noSkills: false, skillsFlags: ["-g", "--all"], }); }); + + it("rejects --force-hooks", () => { + expect(() => parseInitArgs(["--force-hooks"])).toThrow(/--force-hooks was removed/); + }); }); -describeHooks("skeleton init hooks", () => { - it("fresh init writes scaffold, hooks, and scripts", () => { +describe("skeleton init", () => { + it("fresh init writes scaffold, scripts, and pre-commit", () => { const cwd = makeRepo(); const result = runInit({ cwd }); expect(result.scaffold).toBe("created"); expect(existsSync(join(cwd, "skeleton.toml"))).toBe(true); expect(existsSync(join(cwd, ".skeleton/config.yaml"))).toBe(false); expect(existsSync(join(cwd, ".skeleton/registry.md"))).toBe(false); - expect(existsSync(join(cwd, ".skeleton/customize"))).toBe(true); - expect(existsSync(join(cwd, ".cursor/hooks.json"))).toBe(true); - expect(existsSync(join(cwd, ".claude/settings.json"))).toBe(true); + expect(existsSync(join(cwd, ".skeleton/customize"))).toBe(false); + expect(existsSync(join(cwd, ".cursor/hooks.json"))).toBe(false); const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")); expect(pkg.scripts["validate:changed"]).toBe("skeleton validate changed"); expect(existsSync(join(cwd, ".pre-commit-config.yaml"))).toBe(true); @@ -126,104 +71,11 @@ describeHooks("skeleton init hooks", () => { expect(result.precommit).toBe("skipped"); }); - it("idempotent re-run skips unchanged hooks and scaffold", () => { + it("idempotent re-run skips unchanged scaffold", () => { const cwd = makeRepo(); runInit({ cwd }); const result = runInit({ cwd }); expect(result.scaffold).toBe("skipped"); - expect(result.hooks.every((h) => h.action === "skipped")).toBe(true); - }); - - it("preserves unrelated cursor hooks", () => { - const cwd = makeRepo(); - mkdirSync(join(cwd, ".cursor"), { recursive: true }); - writeFileSync( - join(cwd, ".cursor/hooks.json"), - JSON.stringify({ - version: 1, - hooks: { - postToolUse: [{ command: "echo user-hook", matcher: "Write" }], - }, - }), - ); - runInit({ cwd }); - const hooks = JSON.parse(readFileSync(join(cwd, ".cursor/hooks.json"), "utf8")); - expect(hooks.hooks.postToolUse).toHaveLength(2); - expect( - hooks.hooks.postToolUse.some((h: { command: string }) => h.command === "echo user-hook"), - ).toBe(true); - }); - - it("preserves Claude permissions sibling keys", () => { - const cwd = makeRepo(); - mkdirSync(join(cwd, ".claude"), { recursive: true }); - writeFileSync( - join(cwd, ".claude/settings.json"), - JSON.stringify({ permissions: { allow: ["Read"] }, hooks: {} }), - ); - runInit({ cwd }); - const settings = JSON.parse(readFileSync(join(cwd, ".claude/settings.json"), "utf8")); - expect(settings.permissions).toEqual({ allow: ["Read"] }); - }); - - it("skips user-edited skeleton hook unless --force-hooks", () => { - const cwd = makeRepo(); - mkdirSync(join(cwd, ".cursor"), { recursive: true }); - writeFileSync( - join(cwd, ".cursor/hooks.json"), - JSON.stringify({ - version: 1, - hooks: { - postToolUse: [{ command: "node customize-on-skill-read.js", matcher: "Write" }], - }, - }), - ); - const conflict = mergeHookConfigs({ - cwd, - hookCommand: resolveHookCommand(cwd), - }); - expect(conflict[0]?.action).toBe("conflict"); - const forced = mergeHookConfigs({ - cwd, - hookCommand: resolveHookCommand(cwd), - forceHooks: true, - }); - expect(forced[0]?.action).toBe("updated"); - const hooks = JSON.parse(readFileSync(join(cwd, ".cursor/hooks.json"), "utf8")); - expect(hooks.hooks.postToolUse[0].matcher).toBe("Read"); - }); - - it("hard-fails on invalid hook JSON", () => { - const cwd = makeRepo(); - mkdirSync(join(cwd, ".cursor"), { recursive: true }); - writeFileSync(join(cwd, ".cursor/hooks.json"), "{not json"); - expect(() => mergeHookConfigs({ cwd, hookCommand: resolveHookCommand(cwd) })).toThrow( - /Invalid JSON/, - ); - }); - - it("skips codex hooks when .codex directory is missing", () => { - const cwd = makeRepo(); - const results = mergeHookConfigs({ - cwd, - hookCommand: resolveHookCommand(cwd), - }); - const codex = results.find((r) => r.platform === "codex"); - expect(codex?.action).toBe("skipped"); - expect(existsSync(join(cwd, ".codex/hooks.json"))).toBe(false); - }); - - it("re-init upgrades hook command in place without duplicating entries", () => { - const cwd = makeRepo(); - const tsCommand = "bun src/hooks/customize-on-skill-read.ts"; - mergeHookConfigs({ cwd, hookCommand: tsCommand, forceHooks: true }); - const jsCommand = "node dist/hooks/customize-on-skill-read.js"; - mergeHookConfigs({ cwd, hookCommand: jsCommand, forceHooks: true }); - const hooks = JSON.parse(readFileSync(join(cwd, ".cursor/hooks.json"), "utf8")); - const skeletonHooks = hooks.hooks.postToolUse.filter((h: { command: string }) => - isSkeletonHookCommand(h.command), - ); - expect(skeletonHooks).toHaveLength(1); }); it("does not overwrite existing skeleton.toml on re-init", () => { @@ -238,12 +90,6 @@ describeHooks("skeleton init hooks", () => { expect(readFileSync(configPath, "utf8")).toContain("custom"); }); - it("writes .skeleton/customize directory", () => { - const cwd = makeRepo(); - runInit({ cwd }); - expect(existsSync(join(cwd, ".skeleton/customize"))).toBe(true); - }); - it("runs skills add when --skills is requested", () => { const cwd = makeRepo(); const calls: Array<{ args: string[]; cwd: string }> = []; diff --git a/src/__tests__/cli/integration.test.ts b/src/__tests__/cli/integration.test.ts index 39d0feb..9e1818e 100644 --- a/src/__tests__/cli/integration.test.ts +++ b/src/__tests__/cli/integration.test.ts @@ -5,7 +5,6 @@ import { dirname, join } from "node:path"; import { attestDocuments } from "../../audit/core/review-proof.ts"; import { runAudit } from "../../audit/run.ts"; import { checkCatalog, runCatalogCli, writeCatalog } from "../../catalog.ts"; -import { resolveCustomize } from "../../customize/resolve.ts"; import { runBuildPlugin } from "../../plugins/build.ts"; import { codeValidationHint, @@ -71,18 +70,6 @@ exclude = [] }); }); -describe("customize resolve", () => { - it("returns customize file contents for slug", () => { - const result = resolveCustomize(NESTED_SKILLS_CUSTOMIZE, "code-review"); - expect(result.content).toContain("Code review customize"); - }); - - it("returns null for missing slug", () => { - const result = resolveCustomize(NESTED_SKILLS_CUSTOMIZE, "missing-slug"); - expect(result.content).toBeNull(); - }); -}); - describe("audit global scoping", () => { it("skips global rules on path-scoped docs audit", async () => { const exit = await runAudit({ diff --git a/src/__tests__/cli/removed-commands.test.ts b/src/__tests__/cli/removed-commands.test.ts new file mode 100644 index 0000000..97b8f40 --- /dev/null +++ b/src/__tests__/cli/removed-commands.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; + +const CLI = join(import.meta.dir, "../../cli.ts"); + +function runCli(args: string[]): { stderr: string; exitCode: number | null } { + const proc = spawnSync("bun", [CLI, ...args], { encoding: "utf8" }); + return { stderr: proc.stderr, exitCode: proc.status }; +} + +describe("removed overlay commands", () => { + it("rejects customize with migration text", () => { + const result = runCli(["customize", "resolve", "code-review"]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("customize: removed"); + }); + + it("rejects hook with migration text", () => { + const result = runCli(["hook", "customize"]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("hook: removed"); + }); +}); diff --git a/src/audit/__tests__/collect.test.ts b/src/audit/__tests__/collect.test.ts index 7944deb..87568a0 100644 --- a/src/audit/__tests__/collect.test.ts +++ b/src/audit/__tests__/collect.test.ts @@ -16,7 +16,6 @@ import { matchesGlobScope, normalizeRelPath } from "../core/shared.ts"; import { buildSkillIndex, isForeignSkillPath } from "../core/skill-roots.ts"; const FIXTURES = join(import.meta.dir, "fixtures"); -const NESTED_SKILLS_CUSTOMIZE = join(FIXTURES, "nested-skills-customize"); describe("normalizeRelPath", () => { it("strips leading ./ segments and normalizes slashes", () => { @@ -54,13 +53,6 @@ describe("collectScanFiles", () => { expect(rels.some((r) => r.includes("packages/outlier"))).toBe(false); }); - it("collects customize markdown without an explicit scan.include", () => { - const config = loadConfig(NESTED_SKILLS_CUSTOMIZE); - const files = collectScanFiles(config, NESTED_SKILLS_CUSTOMIZE); - const rels = files.map((f) => f.replace(`${NESTED_SKILLS_CUSTOMIZE}/`, "")); - expect(rels).toContain(".skeleton/customize/code-review.md"); - }); - it("dedupes a skill reached through a per-slug symlinked root", () => { const root = join(tmpdir(), `skeleton-symlink-${Date.now()}`); try { diff --git a/src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/customize/code-review.md b/src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/customize/code-review.md deleted file mode 100644 index 2678645..0000000 --- a/src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/customize/code-review.md +++ /dev/null @@ -1,3 +0,0 @@ -**Source of truth for** Code review customize overrides. - -When running `/code-review`, include product context. diff --git a/src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/registry.md b/src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/registry.md deleted file mode 100644 index 270ac52..0000000 --- a/src/audit/__tests__/fixtures/nested-skills-customize/.skeleton/registry.md +++ /dev/null @@ -1,5 +0,0 @@ -# Registry - -| Topic | Canonical file | -|-------|----------------| -| Customize: code-review | [code-review.md](customize/code-review.md) | diff --git a/src/audit/__tests__/review-coverage.test.ts b/src/audit/__tests__/review-coverage.test.ts index 9ecdfcc..a3d30da 100644 --- a/src/audit/__tests__/review-coverage.test.ts +++ b/src/audit/__tests__/review-coverage.test.ts @@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "../config/load.ts"; -import { pathRequiresReviewCoverage } from "../core/review-coverage.ts"; +import { collectReviewCoverageFiles, pathRequiresReviewCoverage } from "../core/review-coverage.ts"; import { evaluateAudit } from "../run.ts"; function makeRepo(marker: string): string { @@ -94,6 +94,40 @@ include = ["**/*.mjs"] } }); + it("drops nested vendor trees from the default coverage set", () => { + const root = makeRepo(""); + try { + writeFileSync( + join(root, "skeleton.toml"), + `daysUntilStale = 365 +[scan] +include = ["docs/**"] +exclude = [] +`, + ); + mkdirSync(join(root, "apps/pkg/node_modules/dep"), { recursive: true }); + mkdirSync(join(root, "apps/pkg/dist"), { recursive: true }); + mkdirSync(join(root, "apps/pkg/.venv/lib"), { recursive: true }); + writeFileSync(join(root, "apps/pkg/node_modules/dep/index.js"), "module.exports = 1;\n"); + writeFileSync(join(root, "apps/pkg/dist/out.js"), "export default 1;\n"); + writeFileSync(join(root, "apps/pkg/.venv/lib/site.py"), "x = 1\n"); + writeFileSync(join(root, "apps/pkg/app.ts"), "export const app = 1;\n"); + + const config = loadConfig(root); + expect(pathRequiresReviewCoverage("apps/pkg/node_modules/dep/index.js", config)).toBe(false); + expect(pathRequiresReviewCoverage("apps/pkg/dist/out.js", config)).toBe(false); + expect(pathRequiresReviewCoverage("apps/pkg/.venv/lib/site.py", config)).toBe(false); + expect(pathRequiresReviewCoverage("apps/pkg/app.ts", config)).toBe(true); + expect(collectReviewCoverageFiles(root, config)).toEqual([ + "apps/pkg/app.ts", + "src/orphan.ts", + "src/owned.ts", + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it("errors on files in the coverage include that no document claims", async () => { const root = makeRepo(""); try { diff --git a/src/audit/config/load.ts b/src/audit/config/load.ts index daa2eb8..6479954 100644 --- a/src/audit/config/load.ts +++ b/src/audit/config/load.ts @@ -13,8 +13,6 @@ const SCHEMA_CANDIDATES = [ join(dirname(fileURLToPath(import.meta.url)), "../../../schemas/config.schema.json"), // dist/cli.js → ../schemas join(dirname(fileURLToPath(import.meta.url)), "../schemas/config.schema.json"), - // dist/hooks/customize-on-skill-read.js → ../../schemas - join(dirname(fileURLToPath(import.meta.url)), "../../schemas/config.schema.json"), ]; export const ROOT_CONFIG_TOML = "skeleton.toml"; diff --git a/src/audit/config/types.ts b/src/audit/config/types.ts index 1c470c0..1515de7 100644 --- a/src/audit/config/types.ts +++ b/src/audit/config/types.ts @@ -10,11 +10,6 @@ export interface DenyConfig { paths?: string[]; } -export interface CustomizeConfig { - /** Basenames under `.skeleton/customize/` appended on every customize inject. */ - alwaysInclude?: string[]; -} - /** * Ownership policy for skill-body linting. * Lockfile `sourceType` other than `local` (e.g. `github`) marks skills foreign by default. @@ -65,7 +60,6 @@ export interface SkeletonConfig { /** Re-read cadence (days) for doc-meta last-reviewed; separate from git edit-behind-review. */ daysUntilStale: number; deny?: DenyConfig; - customize?: CustomizeConfig; skillOwnership?: SkillOwnershipConfig; reviewProof?: ReviewProofConfig; reviewCoverage?: ReviewCoverageConfig; diff --git a/src/audit/core/collect.ts b/src/audit/core/collect.ts index 3b76227..dea1eb0 100644 --- a/src/audit/core/collect.ts +++ b/src/audit/core/collect.ts @@ -7,7 +7,6 @@ import { extractScanRootsFromInclude, matchesGlobScope, normalizeRelPath } from import { isForeignSkillPath, type SkillIndex, skillCollectAugments } from "./skill-roots.ts"; const MARKDOWN_GLOBS = ["**/*.md", "**/*.mdc"]; -const BUILTIN_INCLUDE_PATTERNS = [".skeleton/customize/**"]; function isMarkdownFile(absPath: string): boolean { return absPath.endsWith(".md") || absPath.endsWith(".mdc"); @@ -63,7 +62,7 @@ export function collectScanFiles( skillIndex?: SkillIndex, ): string[] { const exclude = mergedExcludes(config); - const includePatterns = [...BUILTIN_INCLUDE_PATTERNS, ...config.scan.include]; + const includePatterns = [...config.scan.include]; if (skillIndex) { includePatterns.push(...skillCollectAugments(skillIndex)); } diff --git a/src/audit/core/review-coverage.ts b/src/audit/core/review-coverage.ts index 6207a94..dd88752 100644 --- a/src/audit/core/review-coverage.ts +++ b/src/audit/core/review-coverage.ts @@ -18,9 +18,10 @@ export const DEFAULT_REVIEW_COVERAGE_EXCLUDE = [ "**/*.spec.*", "**/fixtures/**", "templates/**", - "dist/**", - "node_modules/**", - ".git/**", + "**/dist/**", + "**/node_modules/**", + "**/.venv/**", + "**/.git/**", ".skeleton/plugins/**", ]; diff --git a/src/audit/policies/load.ts b/src/audit/policies/load.ts index d31142b..3d4a33a 100644 --- a/src/audit/policies/load.ts +++ b/src/audit/policies/load.ts @@ -11,8 +11,6 @@ const SCHEMA_CANDIDATES = [ join(dirname(fileURLToPath(import.meta.url)), "../../../schemas/policy-file.schema.json"), // dist/cli.js → ../schemas join(dirname(fileURLToPath(import.meta.url)), "../schemas/policy-file.schema.json"), - // dist/hooks → ../../schemas - join(dirname(fileURLToPath(import.meta.url)), "../../schemas/policy-file.schema.json"), ]; function resolvePolicySchemaPath(): string { diff --git a/src/cli.ts b/src/cli.ts index 49d1e69..0060c7d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,12 +1,9 @@ #!/usr/bin/env node -import { readFileSync } from "node:fs"; import process from "node:process"; import { findRepoRoot } from "./audit/config/load.ts"; import { parseAuditArgs, runAudit } from "./audit/run.ts"; import { runCatalogCli } from "./catalog.ts"; -import { resolveCustomizeFromRoot } from "./customize/resolve.ts"; -import { runCustomizeHook } from "./hooks/run.ts"; import { runInit } from "./init/init.ts"; import { parseInitArgs } from "./init/parse-args.ts"; import { parseBuildPluginArgs, runBuildPlugin } from "./plugins/build.ts"; @@ -16,16 +13,14 @@ function usage(): void { console.error(`Usage: skeleton Commands: - init [--force-hooks] [--skills] [--no-skills] [skills add flags…] + init [--skills] [--no-skills] [skills add flags…] audit docs|self|skills [--strict] [--json] [--paths=a,b] [--only=rule] [--fix[=doc-meta|anchors|ssot]] [--dry-run] [--confirm-reviewed (doc-meta only; requires --paths)] build-plugin [path] [--check] validate changed [paths…] [--staged] [--base ] catalog [--check] [--strict] write or check .skeleton/catalog.md (gitignored) - customize resolve [--json] - hook customize (reads a host hook payload on stdin) -Note: \`register\` was removed — add a source-of-truth marker to the file and run \`skeleton catalog\`.`); +Note: \`register\`, \`customize\`, and \`hook\` were removed.`); } // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: strict argv parsing enumerates every supported spelling and rejection @@ -107,29 +102,9 @@ function handleCatalog(argv: string[]): number { return runCatalogCli({ check: argv.includes("--check"), strict: argv.includes("--strict") }); } -function handleCustomizeResolve(argv: string[]): number { - const slug = argv[0]; - const json = argv.includes("--json"); - if (!slug) { - console.error("customize resolve: slug required"); - return 1; - } - const result = resolveCustomizeFromRoot(slug); - if (json) { - console.log(JSON.stringify(result, null, 2)); - } else if (result.content) { - process.stdout.write(result.content); - } - return 0; -} - -function handleHook(argv: string[]): number { - if (argv[0] !== "customize") { - usage(); - return 1; - } - process.stdout.write(runCustomizeHook(readFileSync(0, "utf8"))); - return 0; +function handleRemovedOverlay(command: string): number { + console.error(`${command}: removed — overlay inject is gone. Edit the skill in its owning repo.`); + return 1; } function handleInit(argv: string[]): number { @@ -153,9 +128,8 @@ async function dispatchCommand(argv: string[]): Promise { case "catalog": return handleCatalog(rest); case "customize": - return rest[0] === "resolve" ? handleCustomizeResolve(rest.slice(1)) : null; case "hook": - return handleHook(rest); + return handleRemovedOverlay(command); case "init": return handleInit(rest); default: diff --git a/src/customize/__tests__/resolve.test.ts b/src/customize/__tests__/resolve.test.ts deleted file mode 100644 index 8f89f40..0000000 --- a/src/customize/__tests__/resolve.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { resolveCustomize } from "../resolve.ts"; - -function writeConfig(root: string, alwaysInclude: string[]): void { - mkdirSync(join(root, ".skeleton"), { recursive: true }); - const always = - alwaysInclude.length > 0 - ? `\ncustomize:\n alwaysInclude:\n${alwaysInclude.map((n) => ` - ${n}`).join("\n")}\n` - : "\n"; - writeFileSync( - join(root, ".skeleton", "config.yaml"), - `scan:\n include:\n - "docs/**"\n exclude: []\ndaysUntilStale: 180${always}`, - ); -} - -describe("resolveCustomize alwaysInclude", () => { - it("concatenates slug customize and alwaysInclude", () => { - const root = mkdtempSync(join(tmpdir(), "skeleton-customize-")); - try { - writeConfig(root, ["shared-agent-references.md"]); - mkdirSync(join(root, ".skeleton", "customize"), { recursive: true }); - writeFileSync(join(root, ".skeleton", "customize", "code-review.md"), "# slug\n"); - writeFileSync( - join(root, ".skeleton", "customize", "shared-agent-references.md"), - "# shared\n", - ); - - const result = resolveCustomize(root, "code-review"); - expect(result.content).toContain("# slug"); - expect(result.content).toContain("# shared"); - expect(result.content).toContain("---"); - expect(result.included).toEqual([ - ".skeleton/customize/code-review.md", - ".skeleton/customize/shared-agent-references.md", - ]); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it("injects alwaysInclude when slug file is missing", () => { - const root = mkdtempSync(join(tmpdir(), "skeleton-customize-")); - try { - writeConfig(root, ["shared-agent-references.md"]); - mkdirSync(join(root, ".skeleton", "customize"), { recursive: true }); - writeFileSync( - join(root, ".skeleton", "customize", "shared-agent-references.md"), - "# shared only\n", - ); - - const result = resolveCustomize(root, "brand-design"); - expect(result.content).toContain("# shared only"); - expect(result.included).toEqual([".skeleton/customize/shared-agent-references.md"]); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it("returns null when neither slug nor alwaysInclude resolve", () => { - const root = mkdtempSync(join(tmpdir(), "skeleton-customize-")); - try { - writeConfig(root, ["missing.md"]); - mkdirSync(join(root, ".skeleton", "customize"), { recursive: true }); - - const result = resolveCustomize(root, "missing-slug"); - expect(result.content).toBeNull(); - expect(result.included).toEqual([]); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); -}); diff --git a/src/customize/resolve.ts b/src/customize/resolve.ts deleted file mode 100644 index 57d6ee9..0000000 --- a/src/customize/resolve.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { basename, join, relative } from "node:path"; -import { findRepoRoot, loadConfig } from "../audit/config/load.ts"; -import { normalizeRelPath, REGISTRY_DIR_REL } from "../audit/core/shared.ts"; - -const CUSTOMIZE_PREFIX = "Customize: "; - -export interface CustomizeResolveResult { - slug: string; - content: string | null; - path: string | null; - /** Paths under `.skeleton/customize/` that were concatenated into content. */ - included: string[]; -} - -function customizeDir(root: string): string { - return join(root, REGISTRY_DIR_REL, "customize"); -} - -function customizePathForSlug(root: string, slug: string): string { - return join(customizeDir(root), `${slug}.md`); -} - -function resolveSlugFile( - root: string, - slug: string, -): { content: string | null; path: string | null } { - const direct = customizePathForSlug(root, slug); - if (existsSync(direct)) { - return { - content: readFileSync(direct, "utf8"), - path: normalizeRelPath(relative(root, direct)), - }; - } - return { content: null, path: null }; -} - -function alwaysIncludeBasenames(root: string): string[] { - try { - const config = loadConfig(root); - return config.customize?.alwaysInclude ?? []; - } catch { - return []; - } -} - -function readAlwaysInclude( - root: string, - basenames: string[], - skipBasename: string | null, -): { parts: string[]; paths: string[] } { - const parts: string[] = []; - const paths: string[] = []; - const dir = customizeDir(root); - - for (const name of basenames) { - const file = basename(name); - if (skipBasename && file === skipBasename) continue; - const abs = join(dir, file); - if (!existsSync(abs)) continue; - parts.push(readFileSync(abs, "utf8").trimEnd()); - paths.push(normalizeRelPath(relative(root, abs))); - } - - return { parts, paths }; -} - -export function resolveCustomize(root: string, slug: string): CustomizeResolveResult { - const slugFile = resolveSlugFile(root, slug); - const alwaysNames = alwaysIncludeBasenames(root); - const skip = - slugFile.path !== null && slugFile.path !== undefined ? basename(slugFile.path) : null; - const always = readAlwaysInclude(root, alwaysNames, skip); - - const parts: string[] = []; - const included: string[] = []; - - if ( - slugFile.content !== null && - slugFile.content !== undefined && - slugFile.content.trim().length > 0 - ) { - parts.push(slugFile.content.trimEnd()); - if (slugFile.path) included.push(slugFile.path); - } - - parts.push(...always.parts); - included.push(...always.paths); - - if (parts.length === 0) { - return { slug, content: null, path: slugFile.path, included: [] }; - } - - return { - slug, - content: `${parts.join("\n\n---\n\n")}\n`, - path: slugFile.path ?? always.paths[0] ?? null, - included, - }; -} - -export function resolveCustomizeFromRoot(slug: string, startDir?: string): CustomizeResolveResult { - const root = findRepoRoot(startDir); - return resolveCustomize(root, slug); -} - -export { CUSTOMIZE_PREFIX }; diff --git a/src/hooks/customize-on-skill-read.ts b/src/hooks/customize-on-skill-read.ts deleted file mode 100644 index ce20778..0000000 --- a/src/hooks/customize-on-skill-read.ts +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -import { readFileSync } from "node:fs"; -import process from "node:process"; -import { runCustomizeHook } from "./run.ts"; - -process.stdout.write(runCustomizeHook(readFileSync(0, "utf8"))); diff --git a/src/hooks/run.ts b/src/hooks/run.ts deleted file mode 100644 index 2cf74e0..0000000 --- a/src/hooks/run.ts +++ /dev/null @@ -1,98 +0,0 @@ -import process from "node:process"; -import { normalizeRelPath } from "../audit/core/shared.ts"; -import { slugFromPath } from "../audit/core/skill-roots.ts"; -import { resolveCustomizeFromRoot } from "../customize/resolve.ts"; - -interface HookPayload { - tool_name?: string; - toolName?: string; - tool_input?: Record; - toolInput?: Record; - hook_event_name?: string; -} - -function parsePayload(raw: string): HookPayload { - if (!raw.trim()) return {}; - return JSON.parse(raw) as HookPayload; -} - -function extractPath(payload: HookPayload): string | null { - const input = payload.tool_input ?? payload.toolInput ?? {}; - const candidates = [ - input.path, - input.file_path, - input.filePath, - input.target_file, - input.targetFile, - ]; - for (const value of candidates) { - if (typeof value === "string" && value.trim()) return normalizeRelPath(value.trim()); - } - return null; -} - -function extractSkillSlug(payload: HookPayload): string | null { - const tool = payload.tool_name ?? payload.toolName ?? ""; - if (tool === "Skill" || tool === "skill") { - const input = payload.tool_input ?? payload.toolInput ?? {}; - const slug = input.skill ?? input.slug ?? input.name; - if (typeof slug === "string" && slug.trim()) return slug.trim(); - } - const path = extractPath(payload); - if (path) return slugFromPath(path, process.cwd()); - return null; -} - -function cursorResponse(content: string): string { - return JSON.stringify({ additional_context: content }); -} - -function claudeResponse(content: string): string { - return JSON.stringify({ - hookSpecificOutput: { - additionalContext: content, - }, - }); -} - -function codexResponse(content: string): string { - return JSON.stringify({ additionalContext: content }); -} - -function formatResponse(payload: HookPayload, content: string): string { - const tool = payload.tool_name ?? payload.toolName ?? ""; - if (tool === "read_file") return codexResponse(content); - if (payload.hook_event_name?.toLowerCase().includes("claude")) return claudeResponse(content); - if (tool === "Read" && payload.hook_event_name === "postToolUse") return cursorResponse(content); - if (tool === "Read" || tool === "Skill") return claudeResponse(content); - return cursorResponse(content); -} - -/** - * Core customize-on-skill-read hook: parse a host hook payload from `raw`, - * resolve any `.skeleton/customize/.md` override, and return the JSON - * response string to write to stdout. Always resolves to a value (`"{}"` for - * no-op) so callers can pipe it straight out and exit 0. - */ -export function runCustomizeHook(raw: string): string { - try { - const payload = parsePayload(raw); - // Inject on /SKILL.md and skill-tree paths (incl. references/*). - // Still skip Grep/shell (different tools) and non-skill paths (no slug). - const slug = extractSkillSlug(payload); - if (!slug) return "{}"; - - const resolved = resolveCustomizeFromRoot(slug); - if (!resolved.content) return "{}"; - - const from = - resolved.included.length > 0 - ? resolved.included.join(", ") - : (resolved.path ?? ".skeleton/customize"); - const prefix = `\n\n---\nCustomize override for /${slug} (from ${from}):\n\n`; - return formatResponse(payload, prefix + resolved.content); - } catch (error) { - console.error(`customize hook error: ${error}`); - return "{}"; - } -} diff --git a/src/init/init.ts b/src/init/init.ts index 03ed767..04b56fb 100644 --- a/src/init/init.ts +++ b/src/init/init.ts @@ -2,22 +2,15 @@ import { spawnSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import process from "node:process"; -import { - type MergeAction, - type MergeHookResult, - mergeHookConfigs, - mergePackageJsonScripts, -} from "./merge-hooks.ts"; import { mergePrecommitConfig } from "./merge-precommit.ts"; +import { type MergeAction, mergePackageJsonScripts } from "./merge-scripts.ts"; import { resolvePackageRoot, resolveTemplatesDir } from "./package-paths.ts"; -import { resolveHookCommand } from "./resolve-hook-command.ts"; import { skillsAddArgs } from "./skills-args.ts"; const TEMPLATES_DIR = resolveTemplatesDir(); export interface InitOptions { cwd?: string; - forceHooks?: boolean; skills?: boolean; noSkills?: boolean; skillsFlags?: string[]; @@ -26,7 +19,6 @@ export interface InitOptions { export interface InitResult { scaffold: "created" | "skipped"; - hooks: MergeHookResult[]; scripts: MergeAction; skills: "installed" | "skipped"; precommit: MergeAction; @@ -44,7 +36,6 @@ function writeScaffold(cwd: string): "created" | "skipped" { created = true; } - mkdirSync(join(skeletonDir, "customize"), { recursive: true }); return created ? "created" : "skipped"; } @@ -79,17 +70,6 @@ function runSkillsAdd(args: string[], cwd: string): number { return result.status ?? 1; } -function logHookMergeResult(result: MergeHookResult): void { - if (result.action === "conflict") { - console.error( - `init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`, - ); - return; - } - if (result.action === "added") console.log(`init: added ${result.platform} customize hook`); - if (result.action === "updated") console.log(`init: updated ${result.platform} customize hook`); -} - function installSkillsIfRequested(options: InitOptions, cwd: string): InitResult["skills"] { if (!(options.skills && !options.noSkills)) return "skipped"; const args = skillsAddArgs({ skillsFlags: options.skillsFlags }); @@ -105,15 +85,11 @@ export function runInit(options: InitOptions = {}): InitResult { assertPackageResolvable(cwd); const scaffold = writeScaffold(cwd); - const hookCommand = resolveHookCommand(cwd); - const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks }); const scripts = mergePackageJsonScripts(cwd); const precommit = mergePrecommitConfig(cwd); - for (const result of hooks) logHookMergeResult(result); - if (scaffold === "created") { - console.log("init: wrote skeleton.toml (IDE customize hooks optional)"); + console.log("init: wrote skeleton.toml"); } else { console.log("init: skeleton.toml or .skeleton/ already present — skipped scaffold write"); } @@ -129,5 +105,5 @@ export function runInit(options: InitOptions = {}): InitResult { } const skills = installSkillsIfRequested(options, cwd); - return { scaffold, hooks, scripts, skills, precommit }; + return { scaffold, scripts, skills, precommit }; } diff --git a/src/init/merge-hooks.ts b/src/init/merge-hooks.ts deleted file mode 100644 index 0b37557..0000000 --- a/src/init/merge-hooks.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { resolveTemplatesDir } from "./package-paths.ts"; -import { isSkeletonHookCommand } from "./resolve-hook-command.ts"; - -const TEMPLATES_DIR = resolveTemplatesDir(); - -export type MergeAction = "added" | "updated" | "skipped" | "conflict"; - -export interface MergeHookResult { - platform: string; - action: MergeAction; - message?: string; -} - -export interface MergeHooksOptions { - cwd: string; - hookCommand: string; - forceHooks?: boolean; -} - -function identityKey(platform: string, event: string, matcher: string): string { - return `skeleton:customize:${platform}:${event}:${matcher}`; -} - -function loadFragment(name: string, hookCommand: string): unknown { - const raw = readFileSync(join(TEMPLATES_DIR, name), "utf8"); - return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand)); -} - -function readJson(path: string): unknown { - if (!existsSync(path)) return null; - try { - return JSON.parse(readFileSync(path, "utf8")) as unknown; - } catch (error) { - throw new Error(`Invalid JSON in ${path}: ${error}`); - } -} - -function writeJson(path: string, value: unknown): void { - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8"); -} - -function deepEqual(a: unknown, b: unknown): boolean { - return JSON.stringify(a) === JSON.stringify(b); -} - -interface CursorHookEntry { - command?: string; - matcher?: string; - [key: string]: unknown; -} - -interface MergeExistingCursorInput { - postToolUse: CursorHookEntry[]; - skeletonIdx: number; - canonical: CursorHookEntry; - opts: MergeHooksOptions; -} - -function mergeExistingCursorHook(input: MergeExistingCursorInput): MergeHookResult | null { - const { postToolUse, skeletonIdx, canonical, opts } = input; - const current = postToolUse[skeletonIdx]; - const userEdited = - current && - !opts.forceHooks && - (current.matcher !== canonical.matcher || !isSkeletonHookCommand(current.command)); - - if (userEdited) { - return { - platform: "cursor", - action: "conflict", - message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read")), - }; - } - - const extras = Object.fromEntries( - Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)), - ); - const merged = { ...extras, ...canonical }; - if (deepEqual(current, merged)) return { platform: "cursor", action: "skipped" }; - postToolUse[skeletonIdx] = merged; - return null; -} - -interface PersistCursorHooksInput { - targetPath: string; - existing: Record; - hooks: Record; - postToolUse: CursorHookEntry[]; - skeletonIdx: number; -} - -function persistCursorHooks(input: PersistCursorHooksInput): MergeHookResult { - const { targetPath, existing, hooks, postToolUse, skeletonIdx } = input; - const next = { - ...existing, - version: existing.version ?? 1, - hooks: { ...hooks, postToolUse }, - }; - if (deepEqual(existing, next)) return { platform: "cursor", action: "skipped" }; - writeJson(targetPath, next); - return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" }; -} - -function mergeCursorHooks( - targetPath: string, - fragment: { hooks?: { postToolUse?: CursorHookEntry[] } }, - opts: MergeHooksOptions, -): MergeHookResult { - const existing = (readJson(targetPath) as Record | null) ?? {}; - const hooks = (existing.hooks as Record | undefined) ?? {}; - const postToolUse = [...(hooks.postToolUse ?? [])]; - const incoming = fragment.hooks?.postToolUse?.[0]; - if (!incoming) return { platform: "cursor", action: "skipped" }; - - const skeletonIdx = postToolUse.findIndex((entry) => isSkeletonHookCommand(entry.command)); - const canonical = { ...incoming, matcher: incoming.matcher ?? "Read" }; - - if (skeletonIdx >= 0) { - const conflict = mergeExistingCursorHook({ postToolUse, skeletonIdx, canonical, opts }); - if (conflict) return conflict; - } else { - postToolUse.push(canonical); - } - - return persistCursorHooks({ targetPath, existing, hooks, postToolUse, skeletonIdx }); -} - -interface NestedHookEntry { - type?: string; - command?: string; - [key: string]: unknown; -} - -interface MatcherGroup { - matcher?: string; - hooks?: NestedHookEntry[]; - [key: string]: unknown; -} - -interface MergeNestedHooksOptions { - platform: string; - targetPath: string; - fragment: { hooks?: Record }; - eventName: string; - opts: MergeHooksOptions; -} - -interface MergeIncomingHookGroupOptions { - group: MatcherGroup; - incomingHook: NestedHookEntry; - eventHooks: MatcherGroup[]; - opts: MergeHooksOptions; - platform: string; - eventName: string; -} - -interface MergeExistingNestedInput { - hooks: NestedHookEntry[]; - skeletonIdx: number; - incomingHook: NestedHookEntry; - opts: MergeHooksOptions; - platform: string; - eventName: string; - matcher: string; -} - -function mergeExistingNestedSkeletonHook(input: MergeExistingNestedInput): { - changed: boolean; - conflict?: MergeHookResult; -} { - const { hooks, skeletonIdx, incomingHook, opts, platform, eventName, matcher } = input; - const current = hooks[skeletonIdx]; - const userEdited = - current && - !opts.forceHooks && - (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command)); - - if (userEdited) { - return { - changed: false, - conflict: { - platform, - action: "conflict", - message: identityKey(platform, eventName, matcher), - }, - }; - } - - const extras = Object.fromEntries( - Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)), - ); - const merged = { ...extras, ...incomingHook }; - if (!deepEqual(current, merged)) { - hooks[skeletonIdx] = merged; - return { changed: true }; - } - return { changed: false }; -} - -function mergeIncomingHookGroup(args: MergeIncomingHookGroupOptions): { - changed: boolean; - conflict?: MergeHookResult; -} { - const { group, incomingHook, eventHooks, opts, platform, eventName } = args; - const matcher = group.matcher ?? ""; - const groupIdx = eventHooks.findIndex((g) => g.matcher === matcher); - if (groupIdx < 0) { - eventHooks.push({ ...group, hooks: [{ ...incomingHook }] }); - return { changed: true }; - } - - const existingGroup = eventHooks[groupIdx]; - const hooks = [...(existingGroup?.hooks ?? [])]; - const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command)); - - if (skeletonIdx >= 0) { - const result = mergeExistingNestedSkeletonHook({ - hooks, - skeletonIdx, - incomingHook, - opts, - platform, - eventName, - matcher, - }); - if (result.conflict) return { changed: false, conflict: result.conflict }; - eventHooks[groupIdx] = { ...existingGroup, matcher, hooks }; - return { changed: result.changed }; - } - - hooks.push({ ...incomingHook }); - eventHooks[groupIdx] = { ...existingGroup, matcher, hooks }; - return { changed: true }; -} - -function mergeIncomingGroups(args: { - incomingGroups: MatcherGroup[]; - eventHooks: MatcherGroup[]; - opts: MergeHooksOptions; - platform: string; - eventName: string; -}): { changed: boolean; conflict?: MergeHookResult } { - let changed = false; - for (const incomingGroup of args.incomingGroups) { - const incomingHook = incomingGroup.hooks?.[0]; - if (!incomingHook) continue; - - const result = mergeIncomingHookGroup({ - group: incomingGroup, - incomingHook, - eventHooks: args.eventHooks, - opts: args.opts, - platform: args.platform, - eventName: args.eventName, - }); - if (result.conflict) return { changed: false, conflict: result.conflict }; - if (result.changed) changed = true; - } - return { changed }; -} - -function mergeNestedHooks(args: MergeNestedHooksOptions): MergeHookResult { - const { platform, targetPath, eventName } = args; - const existing = (readJson(targetPath) as Record | null) ?? {}; - const rootHooks = (existing.hooks as Record | undefined) ?? {}; - const eventHooks = [...(rootHooks[eventName] ?? [])]; - const incomingGroups = args.fragment.hooks?.[eventName] ?? []; - - const result = mergeIncomingGroups({ - incomingGroups, - eventHooks, - opts: args.opts, - platform, - eventName, - }); - if (result.conflict) return result.conflict; - if (!result.changed) return { platform, action: "skipped" }; - - const next = { - ...existing, - hooks: { ...rootHooks, [eventName]: eventHooks }, - }; - writeJson(targetPath, next); - return { platform, action: "updated" }; -} - -export function mergeHookConfigs(opts: MergeHooksOptions): MergeHookResult[] { - const results: MergeHookResult[] = []; - - const cursorPath = join(opts.cwd, ".cursor/hooks.json"); - const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand) as { - hooks?: { postToolUse?: CursorHookEntry[] }; - }; - results.push(mergeCursorHooks(cursorPath, cursorFragment, opts)); - - const claudePath = join(opts.cwd, ".claude/settings.json"); - const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand) as { - hooks?: Record; - }; - results.push( - mergeNestedHooks({ - platform: "claude", - targetPath: claudePath, - fragment: claudeFragment, - eventName: "PostToolUse", - opts, - }), - ); - - const codexPath = join(opts.cwd, ".codex/hooks.json"); - if (existsSync(join(opts.cwd, ".codex"))) { - const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand) as { - hooks?: Record; - }; - results.push( - mergeNestedHooks({ - platform: "codex", - targetPath: codexPath, - fragment: codexFragment, - eventName: "PostToolUse", - opts, - }), - ); - } else { - results.push({ platform: "codex", action: "skipped", message: "missing .codex directory" }); - } - - return results; -} - -export function mergePackageJsonScripts(cwd: string): MergeAction { - const pkgPath = join(cwd, "package.json"); - if (!existsSync(pkgPath)) return "skipped"; - - const fragment = JSON.parse( - readFileSync(join(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"), - ) as Record; - const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { - scripts?: Record; - }; - pkg.scripts ??= {}; - - let changed = false; - for (const [key, value] of Object.entries(fragment)) { - if (pkg.scripts[key] !== value) { - pkg.scripts[key] = value; - changed = true; - } - } - - if (!changed) return "skipped"; - writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); - return "updated"; -} diff --git a/src/init/merge-precommit.ts b/src/init/merge-precommit.ts index e4f48d9..79fee57 100644 --- a/src/init/merge-precommit.ts +++ b/src/init/merge-precommit.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import type { MergeAction } from "./merge-hooks.ts"; +import type { MergeAction } from "./merge-scripts.ts"; import { resolveTemplatesDir } from "./package-paths.ts"; const TEMPLATES_DIR = resolveTemplatesDir(); diff --git a/src/init/merge-scripts.ts b/src/init/merge-scripts.ts new file mode 100644 index 0000000..a206dd7 --- /dev/null +++ b/src/init/merge-scripts.ts @@ -0,0 +1,32 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { resolveTemplatesDir } from "./package-paths.ts"; + +const TEMPLATES_DIR = resolveTemplatesDir(); + +export type MergeAction = "added" | "updated" | "skipped"; + +export function mergePackageJsonScripts(cwd: string): MergeAction { + const pkgPath = join(cwd, "package.json"); + if (!existsSync(pkgPath)) return "skipped"; + + const fragment = JSON.parse( + readFileSync(join(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"), + ) as Record; + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { + scripts?: Record; + }; + pkg.scripts ??= {}; + + let changed = false; + for (const [key, value] of Object.entries(fragment)) { + if (pkg.scripts[key] !== value) { + pkg.scripts[key] = value; + changed = true; + } + } + + if (!changed) return "skipped"; + writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); + return "updated"; +} diff --git a/src/init/parse-args.ts b/src/init/parse-args.ts index 13b0bea..496bdda 100644 --- a/src/init/parse-args.ts +++ b/src/init/parse-args.ts @@ -1,14 +1,16 @@ -const SKELETON_INIT_FLAGS = new Set(["--force-hooks", "--skills", "--no-skills"]); +const SKELETON_INIT_FLAGS = new Set(["--skills", "--no-skills"]); export interface ParsedInitArgs { - forceHooks: boolean; skills: boolean; noSkills: boolean; skillsFlags: string[]; } export function parseInitArgs(argv: string[]): ParsedInitArgs { - const forceHooks = argv.includes("--force-hooks"); + if (argv.includes("--force-hooks")) { + throw new Error("init: --force-hooks was removed with customize hooks"); + } + const noSkills = argv.includes("--no-skills"); const skills = argv.includes("--skills"); @@ -24,5 +26,5 @@ export function parseInitArgs(argv: string[]): ParsedInitArgs { if (skills) skillsFlags.push(arg); } - return { forceHooks, skills, noSkills, skillsFlags }; + return { skills, noSkills, skillsFlags }; } diff --git a/src/init/resolve-hook-command.ts b/src/init/resolve-hook-command.ts deleted file mode 100644 index 8427993..0000000 --- a/src/init/resolve-hook-command.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { existsSync, realpathSync } from "node:fs"; -import { createRequire } from "node:module"; -import { dirname, join, relative, resolve } from "node:path"; -import { resolvePackageRoot } from "./package-paths.ts"; - -const PACKAGE_NAME = "@csark0812/skeleton"; -const CLI_DIST = "dist/cli.js"; -const PACKAGE_ROOT = resolvePackageRoot(); - -/** Dev command inside this package (mirrors package scripts). */ -const DEV_HOOK_COMMAND = "bun src/cli.ts hook customize"; - -/** Fallback when the package is not yet installed under cwd. */ -const FALLBACK_HOOK_COMMAND = `node node_modules/${PACKAGE_NAME}/${CLI_DIST} hook customize`; - -function safeRealpath(path: string): string { - try { - return realpathSync(path); - } catch { - return path; - } -} - -export function toRepoRelative(cwd: string, absPath: string): string { - // realpath both sides so macOS /var → /private/var does not force an absolute path. - const rel = relative(safeRealpath(cwd), safeRealpath(absPath)).replace(/\\/g, "/"); - return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel; -} - -function tryResolvePublishedCli(cwd: string): string | null { - try { - const req = createRequire(join(cwd, "package.json")); - return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`); - } catch { - return null; - } -} - -function walkNodeModulesCli(cwd: string): string | null { - let dir = cwd; - while (true) { - const candidate = join(dir, "node_modules", PACKAGE_NAME, CLI_DIST); - if (existsSync(candidate)) return candidate; - const parent = dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; -} - -function isInsidePackageRoot(cwd: string): boolean { - const rel = relative(PACKAGE_ROOT, resolve(cwd)).replace(/\\/g, "/"); - return rel === "" || !(rel.startsWith("..") || rel.startsWith("/")); -} - -function nodeCliHookCommand(cliPath: string): string { - return `node ${cliPath} hook customize`; -} - -/** - * Resolve the customize-hook command for `skeleton init`. - * Prefer a cwd-local `node …/dist/cli.js hook customize` so IDE runners that - * lack `node_modules/.bin` on PATH still work after a local install. - */ -export function resolveHookCommand(cwd: string): string { - if (isInsidePackageRoot(cwd)) return DEV_HOOK_COMMAND; - - const published = tryResolvePublishedCli(cwd); - if (published) return nodeCliHookCommand(toRepoRelative(cwd, published)); - - const hoisted = walkNodeModulesCli(cwd); - if (hoisted) return nodeCliHookCommand(toRepoRelative(cwd, hoisted)); - - return FALLBACK_HOOK_COMMAND; -} - -export function isSkeletonHookCommand(command: string | undefined): boolean { - if (!command) return false; - // New CLI form (`… hook customize`, including bare `skeleton hook customize`)… - if (/\bhook\s+customize\b/.test(command)) return true; - // …and the legacy standalone entrypoint, so re-init upgrades it in place. - return /customize-on-skill-read\.(js|ts)\b/.test(command); -} diff --git a/templates/skeleton-init/claude-settings.fragment.json b/templates/skeleton-init/claude-settings.fragment.json deleted file mode 100644 index 7153cfe..0000000 --- a/templates/skeleton-init/claude-settings.fragment.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "hooks": { - "PostToolUse": [ - { - "matcher": "Read", - "hooks": [ - { - "type": "command", - "command": "{{HOOK_COMMAND}}" - } - ] - }, - { - "matcher": "Skill", - "hooks": [ - { - "type": "command", - "command": "{{HOOK_COMMAND}}" - } - ] - } - ] - } -} diff --git a/templates/skeleton-init/codex-hooks.fragment.json b/templates/skeleton-init/codex-hooks.fragment.json deleted file mode 100644 index dc87f6b..0000000 --- a/templates/skeleton-init/codex-hooks.fragment.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "PostToolUse": [ - { - "matcher": "read_file", - "hooks": [ - { - "type": "command", - "command": "{{HOOK_COMMAND}}" - } - ] - } - ] - } -} diff --git a/templates/skeleton-init/config.yaml b/templates/skeleton-init/config.yaml index 1f10de5..78c6bbc 100644 --- a/templates/skeleton-init/config.yaml +++ b/templates/skeleton-init/config.yaml @@ -14,11 +14,6 @@ deny: daysUntilStale: 365 -# Optional: basenames under .skeleton/customize/ appended on every skill inject -# customize: -# alwaysInclude: -# - shared-agent-references.md - # Optional: skill body ownership (skills-lock.json github entries are foreign by default) # skillOwnership: # lockfile: skills-lock.json diff --git a/templates/skeleton-init/cursor-hooks.fragment.json b/templates/skeleton-init/cursor-hooks.fragment.json deleted file mode 100644 index 8b759f9..0000000 --- a/templates/skeleton-init/cursor-hooks.fragment.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "hooks": { - "postToolUse": [ - { - "command": "{{HOOK_COMMAND}}", - "matcher": "Read" - } - ] - } -} diff --git a/templates/skeleton-init/skeleton.toml b/templates/skeleton-init/skeleton.toml index e06f230..17d2893 100644 --- a/templates/skeleton-init/skeleton.toml +++ b/templates/skeleton-init/skeleton.toml @@ -22,11 +22,7 @@ mode = "hash" # Defaults apply when this section is omitted. Empty include disables the gate. # [reviewCoverage] # include = ["**/*.{ts,tsx,js,jsx,mjs,cjs,py}", "package.json", "project.json"] -# exclude = ["**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/fixtures/**", "templates/**"] - -# Optional: basenames under .skeleton/customize/ appended on every skill inject -# [customize] -# alwaysInclude = ["shared-agent-references.md"] +# exclude = ["**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/fixtures/**", "templates/**", "**/node_modules/**", "**/dist/**"] # Optional: near-dupe / SSOT-summary tunables # [docsLint] From 91cbf2156985489504b539d1652ff21b1a95669d Mon Sep 17 00:00:00 2001 From: Christopher Sarkissian Date: Sun, 13 Sep 2026 15:27:01 -0700 Subject: [PATCH 2/3] Record review proof after the 4.0.2 package.json merge. Cherry-pick onto main changed package.json bytes and invalidated the AGENTS and README hashes. Co-authored-by: Cursor --- .skeleton/review-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.skeleton/review-lock.json b/.skeleton/review-lock.json index a1cc9a6..c713b0d 100644 --- a/.skeleton/review-lock.json +++ b/.skeleton/review-lock.json @@ -5,7 +5,7 @@ "reviewedAt": "2026-09-13", "documentHash": "sha256:d80bd8595987f637e889ca2e9bd3e439c06cd7ed18afe59d295b57db53acd8e9", "reviewDependencies": { - "package.json": "sha256:2af98c1fe0094150ad0c43ae55703536e59057bc942f44cb1c50cf87a502fb30", + "package.json": "sha256:f2466500367e981801284bdf5a2582e121e1a2f88ae07bea1c79a4dbcea50b3b", "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79" } }, @@ -13,7 +13,7 @@ "reviewedAt": "2026-09-13", "documentHash": "sha256:a7cc81e0e5a42deaf00c6c9826629d43ea7b981f7c496176748858bce353975e", "reviewDependencies": { - "package.json": "sha256:2af98c1fe0094150ad0c43ae55703536e59057bc942f44cb1c50cf87a502fb30", + "package.json": "sha256:f2466500367e981801284bdf5a2582e121e1a2f88ae07bea1c79a4dbcea50b3b", "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79" } }, From b05c1a795d32eb776ed57cb4e0d9cf72eaae7b24 Mon Sep 17 00:00:00 2001 From: Christopher Sarkissian Date: Sun, 13 Sep 2026 15:28:44 -0700 Subject: [PATCH 3/3] Skip uncovered-path checks for deleted files. CI --base failed on removed customize sources that no paper still claims. Co-authored-by: Cursor --- .skeleton/review-lock.json | 12 ++++++------ AGENTS.md | 2 +- docs/developer/troubleshooting.md | 2 +- docs/developer/validation.md | 6 +++--- src/__tests__/cli/validate-hook.test.ts | 20 ++++++++++++++++++++ src/validate/changed.ts | 5 +++-- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.skeleton/review-lock.json b/.skeleton/review-lock.json index c713b0d..a0d229d 100644 --- a/.skeleton/review-lock.json +++ b/.skeleton/review-lock.json @@ -3,7 +3,7 @@ "documents": { "AGENTS.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:d80bd8595987f637e889ca2e9bd3e439c06cd7ed18afe59d295b57db53acd8e9", + "documentHash": "sha256:aebd013b628edf711e3c300e23a85ad413814735a6226df8c90bd79a246ae2f3", "reviewDependencies": { "package.json": "sha256:f2466500367e981801284bdf5a2582e121e1a2f88ae07bea1c79a4dbcea50b3b", "src/cli.ts": "sha256:c1ae5e72307096cbcfdef93a5144fc2286525348a551bf6af6e86d784d9d6a79" @@ -127,19 +127,19 @@ }, "docs/developer/troubleshooting.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:039007b44df0f025b1b1c544402148e12069b4c7df0706c7786306d5f31a17d5", + "documentHash": "sha256:87a98e05206f9d4376022e358204d3e8508e0d18cffd790e1627a6d84f803bc5", "reviewDependencies": { "src/audit/run.ts": "sha256:01ee82a04363f23ad4d22b44d8773e7637fc08aecdfc54eb16d3feb8b177ead7", - "src/validate/changed.ts": "sha256:231ac839e2411a1826598a470a29a69f14387af3c6e0c115b73224b98500dd9a", + "src/validate/changed.ts": "sha256:2d651eddbab2ae064f56649a7b094766beb43fff45f48849c53506d75eaacd49", "src/validate/git-diff.ts": "sha256:b2c18f555d54d1bda79f3f246561640bb8ee3f5098980bbd8e16dfec283c2df4", "src/validate/staged.ts": "sha256:50e512692be282c68f9568195c733e69d3b7c29eb9f1bec5f4bc12445e20aeb8" } }, "docs/developer/validation.md": { "reviewedAt": "2026-09-13", - "documentHash": "sha256:ce9da365562eaa63f860a96315c03d48755704f29b0f6c4ffb2c41bb6b98f41a", + "documentHash": "sha256:8fb7fefb248562f954dd1069c2ea92cbdf811edbc2f43a0361d1c5384625b94a", "reviewDependencies": { - "src/validate/changed.ts": "sha256:231ac839e2411a1826598a470a29a69f14387af3c6e0c115b73224b98500dd9a", + "src/validate/changed.ts": "sha256:2d651eddbab2ae064f56649a7b094766beb43fff45f48849c53506d75eaacd49", "src/validate/git-diff.ts": "sha256:b2c18f555d54d1bda79f3f246561640bb8ee3f5098980bbd8e16dfec283c2df4", "src/validate/staged.ts": "sha256:50e512692be282c68f9568195c733e69d3b7c29eb9f1bec5f4bc12445e20aeb8" } @@ -150,7 +150,7 @@ "reviewDependencies": { "docs/developer/getting-started.md": "sha256:e62fa3c2a9449ab21363019c4551ad0d34278cd8a9de13ba88a4408b3bae6111", "docs/developer/install.md": "sha256:bfba088b3cf9a7733dde57c833c74c0863e0cfe5b68ed252165fa2d588d49924", - "docs/developer/validation.md": "sha256:ce9da365562eaa63f860a96315c03d48755704f29b0f6c4ffb2c41bb6b98f41a" + "docs/developer/validation.md": "sha256:8fb7fefb248562f954dd1069c2ea92cbdf811edbc2f43a0361d1c5384625b94a" } }, "skeleton/SKILL.md": { diff --git a/AGENTS.md b/AGENTS.md index b01a444..4bec42d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ bun test ./tests/smoke.test.ts | Foreign / lockfile-synced skill body | skipped — lint in the owning skills/toolbox repo (`skills-lock.json` / `skillOwnership`) | | TypeScript under `src/` | `bun test` + `bun run typecheck` + `bun run build`. `validate:changed` fails uncovered paths | -`validate:changed` classifies code paths and leaves correctness to `bun test` + `typecheck` + `build`. It scans `review-deps` and audits every linked document. A coverage-candidate path with no owning paper fails with `uncovered-changed-path` on local and `--base` runs. Mixed commits do not hide that. Hash review proof invalidates a paper when dependency bytes change. Date mode requires the paper in the change set with today's review date. `--staged` reads index bytes and fails `stage-required` when an impacted paper or the hash lockfile differs from HEAD and is not staged. Owned skill paths run the skills suite. Wired policy YAML runs full docs plus owned-skill prose. Foreign lockfile skills are skipped. Other `.skeleton/**` YAML (not `config.yaml`) fails if not wired to a plugin. Missing explicit paths also exit non-zero. +`validate:changed` classifies code paths and leaves correctness to `bun test` + `typecheck` + `build`. It scans `review-deps` and audits every linked document. A live coverage-candidate path with no owning paper fails with `uncovered-changed-path` on local and `--base` runs. Deleted files do not. Mixed commits do not hide that. Hash review proof invalidates a paper when dependency bytes change. Date mode requires the paper in the change set with today's review date. `--staged` reads index bytes and fails `stage-required` when an impacted paper or the hash lockfile differs from HEAD and is not staged. Owned skill paths run the skills suite. Wired policy YAML runs full docs plus owned-skill prose. Foreign lockfile skills are skipped. Other `.skeleton/**` YAML (not `config.yaml`) fails if not wired to a plugin. Missing explicit paths also exit non-zero. Never bump `last-reviewed` as a mechanical cleanup. After a complete re-read, attest only explicit paths: diff --git a/docs/developer/troubleshooting.md b/docs/developer/troubleshooting.md index fcf9e7e..51eb7f5 100644 --- a/docs/developer/troubleshooting.md +++ b/docs/developer/troubleshooting.md @@ -12,7 +12,7 @@ Decision table and routing: [validation](validation.md). Day-one setup: [getting ## `uncovered-changed-path` -**Cause:** A coverage-candidate file changed and no scanned paper lists it in `review-deps`. +**Cause:** A live coverage-candidate file changed and no scanned paper lists it in `review-deps`. Deleted files do not fire this code. **Fix:** Add a `review-deps` path or glob on the owning paper. Then re-read that paper and attest it. diff --git a/docs/developer/validation.md b/docs/developer/validation.md index ff2aa0c..8723f7f 100644 --- a/docs/developer/validation.md +++ b/docs/developer/validation.md @@ -6,7 +6,7 @@ -Router for changed paths: `runValidateChanged` / `evaluateValidateChanged` (`ValidateChangedOptions`). Code paths get a `codeValidationHint` for native gates. Any changed path can drive `review-deps` document-impact discovery. A coverage candidate with no owning paper fails with `uncovered-changed-path` on local and `--base` runs. Package-manager detection may mention `bun` / `npm` / `pnpm` / `yarn`. +Router for changed paths: `runValidateChanged` / `evaluateValidateChanged` (`ValidateChangedOptions`). Code paths get a `codeValidationHint` for native gates. Any changed path can drive `review-deps` document-impact discovery. A live coverage candidate with no owning paper fails with `uncovered-changed-path` on local and `--base` runs. Deleted files do not. Package-manager detection may mention `bun` / `npm` / `pnpm` / `yarn`. ## When you changed X, run Y @@ -42,7 +42,7 @@ skeleton validate changed --base origin/main # CI merge-base diff | `.sh`, `.bash`, `.zsh` | shellcheck or `bash -n` | | Other `.json` | JSONC-tolerant syntax check | | Any changed repository file | discover and audit scanned docs whose `review-deps` path or glob matches | -| Coverage-candidate code or `package.json` / `project.json` | fail `uncovered-changed-path` when no scanned paper claims the path | +| Coverage-candidate code or `package.json` / `project.json` | fail `uncovered-changed-path` when the file still exists and no scanned paper claims it | ### Code paths and impacted documents @@ -50,7 +50,7 @@ Skeleton does not claim to validate application code. It classifies code paths, - Hash mode: changed dependency bytes make the linked document's `review-proof` entry invalid until explicit re-review and attestation. - Date mode: a linked document must be included in the changed set and carry today's explicit review date. -- No linked document: a coverage-candidate path fails with `uncovered-changed-path` on local and `--base` runs. Mixed commits do not hide this. +- No linked document: a live coverage-candidate path fails with `uncovered-changed-path` on local and `--base` runs. Mixed commits do not hide this. Deleted files do not fail that gate. - `--staged`: read document, lockfile, and dependency bytes from the git index. If an impacted paper or the hash lockfile differs from HEAD and is not staged, fail `stage-required`. In this repo: diff --git a/src/__tests__/cli/validate-hook.test.ts b/src/__tests__/cli/validate-hook.test.ts index 02cea9f..81980c2 100644 --- a/src/__tests__/cli/validate-hook.test.ts +++ b/src/__tests__/cli/validate-hook.test.ts @@ -171,6 +171,26 @@ describe("validate changed hook gates", () => { ); }); + it("does not flag deleted coverage files as uncovered", async () => { + const root = makeRoot(); + writeToml(root); + writeOwnedDoc(root); + mkdirSync(join(root, "src"), { recursive: true }); + writeFileSync(join(root, "src/gone.ts"), "export const gone = 1;\n"); + runGit(root, ["init"]); + runGit(root, ["add", "-A"]); + runGit(root, ["commit", "-m", "init"]); + runGit(root, ["rm", "src/gone.ts"]); + runGit(root, ["commit", "-m", "remove gone"]); + + const result = await evaluateValidateChanged({ + root, + base: "HEAD~1", + }); + expect(result.diagnostics.some((item) => item.code === "uncovered-changed-path")).toBe(false); + expect(result.diagnostics.some((item) => item.file === "src/gone.ts")).toBe(false); + }); + it("fails --staged when attested lockfile and document stay unstaged", async () => { const root = makeRoot(); writeToml( diff --git a/src/validate/changed.ts b/src/validate/changed.ts index ea89967..e03511b 100644 --- a/src/validate/changed.ts +++ b/src/validate/changed.ts @@ -771,7 +771,8 @@ export async function evaluateValidateChanged( skillIndex, fileSource, }); - const coverageCandidateCount = relPaths.filter((path) => + const livePaths = relPaths.filter((path) => !resolvedPaths.deleted.has(path)); + const coverageCandidateCount = livePaths.filter((path) => pathRequiresReviewCoverage(path, config), ).length; const diagnostics = [ @@ -781,7 +782,7 @@ export async function evaluateValidateChanged( base: options.base, coverageCandidateCount, }), - ...uncoveredChangedPathDiagnostics(relPaths, config, ownerPatterns), + ...uncoveredChangedPathDiagnostics(livePaths, config, ownerPatterns), ...stageRequiredDiagnostics({ staged: options.staged ?? false, stagedPaths: relPaths,