coding-agent-skills 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/AGENTS.md +44 -0
  2. package/CHANGELOG.md +205 -0
  3. package/CONTRIBUTING.md +54 -0
  4. package/LICENSE +21 -0
  5. package/README.md +85 -0
  6. package/ROADMAP.md +87 -0
  7. package/RUNBOOK.md +47 -0
  8. package/bin/coding-agent-skills +75 -0
  9. package/contracts/evidence-pack/README.md +22 -0
  10. package/contracts/evidence-pack/evidence-pack.example.json +60 -0
  11. package/contracts/evidence-pack/evidence-pack.example.md +49 -0
  12. package/contracts/evidence-pack/evidence-pack.schema.json +156 -0
  13. package/docs/adapters/README.md +82 -0
  14. package/docs/adapters/discovery.md +50 -0
  15. package/docs/adapters/external-adapters.md +42 -0
  16. package/docs/adapters/project-installation.md +135 -0
  17. package/docs/adapters/real-project-adoption.md +193 -0
  18. package/docs/adapters/upgrade-evidence.md +67 -0
  19. package/docs/adapters/upgrades.md +83 -0
  20. package/docs/architecture/README.md +23 -0
  21. package/docs/authoring/README.md +54 -0
  22. package/docs/evidence-bundles/README.md +94 -0
  23. package/docs/privacy/README.md +26 -0
  24. package/docs/release/README.md +42 -0
  25. package/docs/release/npm-package.md +85 -0
  26. package/docs/safety/README.md +94 -0
  27. package/docs/testing/README.md +100 -0
  28. package/docs/usage/README.md +89 -0
  29. package/docs/versioning/README.md +30 -0
  30. package/docs/versioning/adapter-compatibility.md +54 -0
  31. package/examples/README.md +12 -0
  32. package/examples/adapters/README.md +9 -0
  33. package/examples/adapters/documentation-precedence.json +62 -0
  34. package/examples/adapters/narrow-repo-map.json +64 -0
  35. package/examples/adapters/runtime-status-hints.json +76 -0
  36. package/examples/command-policies/README.md +3 -0
  37. package/examples/command-policies/build-verify.json +57 -0
  38. package/examples/command-policies/git-preflight.json +44 -0
  39. package/examples/command-policies/llm-drift-control.json +45 -0
  40. package/examples/command-policies/repo-map.json +59 -0
  41. package/examples/command-policies/runtime-truth.json +59 -0
  42. package/examples/evidence-packs/README.md +3 -0
  43. package/examples/evidence-packs/build-verify.json +68 -0
  44. package/examples/evidence-packs/git-preflight.json +55 -0
  45. package/examples/evidence-packs/llm-drift-control.json +55 -0
  46. package/examples/evidence-packs/repo-map.json +55 -0
  47. package/examples/evidence-packs/runtime-truth.json +55 -0
  48. package/examples/manifests/README.md +3 -0
  49. package/examples/manifests/build-verify.json +14 -0
  50. package/examples/manifests/git-preflight.json +14 -0
  51. package/examples/manifests/llm-drift-control.json +14 -0
  52. package/examples/manifests/repo-map.json +14 -0
  53. package/examples/manifests/runtime-truth.json +14 -0
  54. package/examples/upgrade-evidence/README.md +14 -0
  55. package/examples/upgrade-evidence/chain-fail.evidence.json +155 -0
  56. package/examples/upgrade-evidence/chain-fail.evidence.md +14 -0
  57. package/examples/upgrade-evidence/chain-pass.evidence.json +156 -0
  58. package/examples/upgrade-evidence/stale-pin.evidence.json +117 -0
  59. package/examples/upgrade-evidence/unsafe-upgrade.evidence.json +128 -0
  60. package/examples/upgrade-evidence/valid-upgrade.evidence.json +105 -0
  61. package/examples/upgrade-evidence/valid-upgrade.evidence.md +13 -0
  62. package/examples/workflows/README.md +3 -0
  63. package/examples/workflows/build-verify.md +20 -0
  64. package/examples/workflows/git-preflight.md +18 -0
  65. package/examples/workflows/llm-drift-control.md +16 -0
  66. package/examples/workflows/repo-map.md +20 -0
  67. package/examples/workflows/runtime-truth.md +17 -0
  68. package/package.json +58 -0
  69. package/runs/skill-runs.md +162 -0
  70. package/schemas/adapter-upgrade-evidence.schema.json +443 -0
  71. package/schemas/archive-index.schema.json +174 -0
  72. package/schemas/archive-report.schema.json +322 -0
  73. package/schemas/command-policy.schema.json +125 -0
  74. package/schemas/evidence-bundle.schema.json +394 -0
  75. package/schemas/project-adapter-installation.schema.json +127 -0
  76. package/schemas/project-adapter.schema.json +328 -0
  77. package/schemas/skill-manifest.schema.json +40 -0
  78. package/scripts/check-adapter-upgrade-chain.mjs +32 -0
  79. package/scripts/check-adapter-upgrade.mjs +31 -0
  80. package/scripts/lib/adapter-discovery.mjs +441 -0
  81. package/scripts/lib/adapter-repo-map.mjs +358 -0
  82. package/scripts/lib/adapter-upgrade-chain.mjs +261 -0
  83. package/scripts/lib/adapter-upgrade.mjs +434 -0
  84. package/scripts/lib/evidence-bundle.mjs +831 -0
  85. package/scripts/lib/pack-rules.mjs +704 -0
  86. package/scripts/lib/project-adapter-installation.mjs +327 -0
  87. package/scripts/lib/safe-evidence-output.mjs +92 -0
  88. package/scripts/lib/schema-validator.mjs +146 -0
  89. package/scripts/lib/semver.mjs +54 -0
  90. package/scripts/lib/upgrade-evidence.mjs +276 -0
  91. package/scripts/render-adapter-repo-map.mjs +8 -0
  92. package/scripts/render-evidence-archive-report.mjs +18 -0
  93. package/scripts/run-next +220 -0
  94. package/scripts/test-pack.mjs +2232 -0
  95. package/scripts/validate-adapters.mjs +10 -0
  96. package/scripts/validate-maintainer-loop.mjs +146 -0
  97. package/scripts/validate-pack.mjs +950 -0
  98. package/scripts/validate-project-adapters.mjs +8 -0
  99. package/scripts/verify-evidence-bundle.mjs +18 -0
  100. package/skills/build-verify/SKILL.md +62 -0
  101. package/skills/build-verify/adapter-interface.md +7 -0
  102. package/skills/build-verify/agents/openai.yaml +4 -0
  103. package/skills/build-verify/checklist.md +12 -0
  104. package/skills/build-verify/evidence-template.md +11 -0
  105. package/skills/build-verify/examples.md +16 -0
  106. package/skills/build-verify/failure-modes.md +14 -0
  107. package/skills/git-preflight/SKILL.md +65 -0
  108. package/skills/git-preflight/adapter-interface.md +7 -0
  109. package/skills/git-preflight/agents/openai.yaml +4 -0
  110. package/skills/git-preflight/checklist.md +11 -0
  111. package/skills/git-preflight/evidence-template.md +10 -0
  112. package/skills/git-preflight/examples.md +18 -0
  113. package/skills/git-preflight/failure-modes.md +13 -0
  114. package/skills/llm-drift-control/SKILL.md +67 -0
  115. package/skills/llm-drift-control/adapter-interface.md +7 -0
  116. package/skills/llm-drift-control/agents/openai.yaml +4 -0
  117. package/skills/llm-drift-control/checklist.md +11 -0
  118. package/skills/llm-drift-control/evidence-template.md +13 -0
  119. package/skills/llm-drift-control/examples.md +15 -0
  120. package/skills/llm-drift-control/failure-modes.md +13 -0
  121. package/skills/repo-map/SKILL.md +71 -0
  122. package/skills/repo-map/adapter-interface.md +18 -0
  123. package/skills/repo-map/agents/openai.yaml +4 -0
  124. package/skills/repo-map/checklist.md +15 -0
  125. package/skills/repo-map/evidence-template.md +29 -0
  126. package/skills/repo-map/examples.md +19 -0
  127. package/skills/repo-map/failure-modes.md +16 -0
  128. package/skills/runtime-truth/SKILL.md +62 -0
  129. package/skills/runtime-truth/adapter-interface.md +7 -0
  130. package/skills/runtime-truth/agents/openai.yaml +4 -0
  131. package/skills/runtime-truth/checklist.md +11 -0
  132. package/skills/runtime-truth/evidence-template.md +12 -0
  133. package/skills/runtime-truth/examples.md +20 -0
  134. package/skills/runtime-truth/failure-modes.md +13 -0
  135. package/tests/README.md +44 -0
  136. package/tests/adapters/README.md +15 -0
  137. package/tests/completion/README.md +15 -0
  138. package/tests/evidence/README.md +15 -0
  139. package/tests/fixtures/README.md +23 -0
  140. package/tests/fixtures/adapters/allow-deploy.json +60 -0
  141. package/tests/fixtures/adapters/allow-git-push.json +60 -0
  142. package/tests/fixtures/adapters/expand-scope.json +53 -0
  143. package/tests/fixtures/adapters/expose-secrets.json +53 -0
  144. package/tests/fixtures/adapters/incompatible-version.json +53 -0
  145. package/tests/fixtures/adapters/override-audit-only.json +53 -0
  146. package/tests/fixtures/adapters/redefine-completion.json +53 -0
  147. package/tests/fixtures/adapters/remove-required-evidence.json +53 -0
  148. package/tests/fixtures/adapters/suppress-failures.json +53 -0
  149. package/tests/fixtures/adapters/valid-narrowing.json +53 -0
  150. package/tests/fixtures/adapters/valid-repo-map.json +53 -0
  151. package/tests/fixtures/adapters/weakening-repo-map.json +42 -0
  152. package/tests/fixtures/completion/cases.json +143 -0
  153. package/tests/fixtures/completion/false-complete.json +51 -0
  154. package/tests/fixtures/evidence-bundles/advisory-review-soon/archive/evidence-archive-index.json +52 -0
  155. package/tests/fixtures/evidence-bundles/advisory-review-soon/evidence/repo-map.evidence.json +68 -0
  156. package/tests/fixtures/evidence-bundles/advisory-review-soon/evidence/valid-upgrade.evidence.json +105 -0
  157. package/tests/fixtures/evidence-bundles/advisory-review-soon/evidence-bundle.json +109 -0
  158. package/tests/fixtures/evidence-bundles/invalid-archive/archive/evidence-archive-index.json +52 -0
  159. package/tests/fixtures/evidence-bundles/invalid-archive/evidence/repo-map.evidence.json +68 -0
  160. package/tests/fixtures/evidence-bundles/invalid-archive/evidence/valid-upgrade.evidence.json +105 -0
  161. package/tests/fixtures/evidence-bundles/invalid-archive/evidence-bundle.json +109 -0
  162. package/tests/fixtures/evidence-bundles/invalid-archive-index/archive/evidence-archive-index.json +52 -0
  163. package/tests/fixtures/evidence-bundles/invalid-archive-index/evidence/repo-map.evidence.json +68 -0
  164. package/tests/fixtures/evidence-bundles/invalid-archive-index/evidence/valid-upgrade.evidence.json +105 -0
  165. package/tests/fixtures/evidence-bundles/invalid-archive-index/evidence-bundle.json +109 -0
  166. package/tests/fixtures/evidence-bundles/invalid-hash/archive/evidence-archive-index.json +52 -0
  167. package/tests/fixtures/evidence-bundles/invalid-hash/evidence/repo-map.evidence.json +68 -0
  168. package/tests/fixtures/evidence-bundles/invalid-hash/evidence/valid-upgrade.evidence.json +105 -0
  169. package/tests/fixtures/evidence-bundles/invalid-hash/evidence-bundle.json +109 -0
  170. package/tests/fixtures/evidence-bundles/invalid-missing-entry/archive/evidence-archive-index.json +52 -0
  171. package/tests/fixtures/evidence-bundles/invalid-missing-entry/evidence/repo-map.evidence.json +68 -0
  172. package/tests/fixtures/evidence-bundles/invalid-missing-entry/evidence/valid-upgrade.evidence.json +105 -0
  173. package/tests/fixtures/evidence-bundles/invalid-missing-entry/evidence-bundle.json +109 -0
  174. package/tests/fixtures/evidence-bundles/invalid-path/archive/evidence-archive-index.json +52 -0
  175. package/tests/fixtures/evidence-bundles/invalid-path/evidence/repo-map.evidence.json +68 -0
  176. package/tests/fixtures/evidence-bundles/invalid-path/evidence/valid-upgrade.evidence.json +105 -0
  177. package/tests/fixtures/evidence-bundles/invalid-path/evidence-bundle.json +109 -0
  178. package/tests/fixtures/evidence-bundles/invalid-provenance/archive/evidence-archive-index.json +52 -0
  179. package/tests/fixtures/evidence-bundles/invalid-provenance/evidence/repo-map.evidence.json +68 -0
  180. package/tests/fixtures/evidence-bundles/invalid-provenance/evidence/valid-upgrade.evidence.json +105 -0
  181. package/tests/fixtures/evidence-bundles/invalid-provenance/evidence-bundle.json +109 -0
  182. package/tests/fixtures/evidence-bundles/invalid-regression/archive/evidence-archive-index.json +52 -0
  183. package/tests/fixtures/evidence-bundles/invalid-regression/evidence/repo-map.evidence.json +68 -0
  184. package/tests/fixtures/evidence-bundles/invalid-regression/evidence/valid-upgrade.evidence.json +105 -0
  185. package/tests/fixtures/evidence-bundles/invalid-regression/evidence-bundle.json +113 -0
  186. package/tests/fixtures/evidence-bundles/invalid-retention/archive/evidence-archive-index.json +52 -0
  187. package/tests/fixtures/evidence-bundles/invalid-retention/evidence/repo-map.evidence.json +68 -0
  188. package/tests/fixtures/evidence-bundles/invalid-retention/evidence/valid-upgrade.evidence.json +105 -0
  189. package/tests/fixtures/evidence-bundles/invalid-retention/evidence-bundle.json +109 -0
  190. package/tests/fixtures/evidence-bundles/invalid-signature-plan/archive/evidence-archive-index.json +52 -0
  191. package/tests/fixtures/evidence-bundles/invalid-signature-plan/evidence/repo-map.evidence.json +68 -0
  192. package/tests/fixtures/evidence-bundles/invalid-signature-plan/evidence/valid-upgrade.evidence.json +105 -0
  193. package/tests/fixtures/evidence-bundles/invalid-signature-plan/evidence-bundle.json +109 -0
  194. package/tests/fixtures/evidence-bundles/valid-bundle/archive/evidence-archive-index.json +52 -0
  195. package/tests/fixtures/evidence-bundles/valid-bundle/evidence/repo-map.evidence.json +68 -0
  196. package/tests/fixtures/evidence-bundles/valid-bundle/evidence/valid-upgrade.evidence.json +105 -0
  197. package/tests/fixtures/evidence-bundles/valid-bundle/evidence-bundle.json +109 -0
  198. package/tests/fixtures/external-adapters/empty/README.md +3 -0
  199. package/tests/fixtures/external-adapters/invalid-completion-override/.coding-agent/adapters/completion/adapter.json +53 -0
  200. package/tests/fixtures/external-adapters/invalid-deploy/.coding-agent/adapters/deploy/adapter.json +60 -0
  201. package/tests/fixtures/external-adapters/invalid-evidence-suppression/.coding-agent/adapters/evidence/adapter.json +53 -0
  202. package/tests/fixtures/external-adapters/invalid-failure-suppression/.coding-agent/adapters/failures/adapter.json +53 -0
  203. package/tests/fixtures/external-adapters/invalid-git-push/.coding-agent/adapters/publish/adapter.json +60 -0
  204. package/tests/fixtures/external-adapters/invalid-malformed/.coding-agent/adapters/malformed/adapter.json +1 -0
  205. package/tests/fixtures/external-adapters/invalid-malformed/malformed-adapter.txt +1 -0
  206. package/tests/fixtures/external-adapters/invalid-mode-escalation/.coding-agent/adapters/mode/adapter.json +53 -0
  207. package/tests/fixtures/external-adapters/invalid-path-traversal/.coding-agent/adapters/path/adapter.json +53 -0
  208. package/tests/fixtures/external-adapters/invalid-restriction-removal/.coding-agent/adapters/restrictions/adapter.json +52 -0
  209. package/tests/fixtures/external-adapters/invalid-scope-expansion/.coding-agent/adapters/scope/adapter.json +53 -0
  210. package/tests/fixtures/external-adapters/invalid-secret-exposure/.coding-agent/adapters/secrets/adapter.json +53 -0
  211. package/tests/fixtures/external-adapters/invalid-skill-id/.coding-agent/adapters/skill/adapter.json +53 -0
  212. package/tests/fixtures/external-adapters/invalid-skill-version/.coding-agent/adapters/skill-version/adapter.json +53 -0
  213. package/tests/fixtures/external-adapters/invalid-unknown-manifest/.coding-agent/adapters/unknown/manifest.json +1 -0
  214. package/tests/fixtures/external-adapters/invalid-version/.coding-agent/adapters/version/adapter.json +53 -0
  215. package/tests/fixtures/external-adapters/mixed/.coding-agent/adapters/invalid/adapter.json +60 -0
  216. package/tests/fixtures/external-adapters/mixed/.coding-agent/adapters/valid/adapter.json +53 -0
  217. package/tests/fixtures/external-adapters/valid-basic/.coding-agent/adapters/basic/adapter.json +53 -0
  218. package/tests/fixtures/external-adapters/valid-doc-precedence/coding-agent/adapters/docs/adapter.json +53 -0
  219. package/tests/fixtures/external-adapters/valid-runtime-status/adapters/coding-agent/runtime/adapter.json +65 -0
  220. package/tests/fixtures/mutation/cases.json +87 -0
  221. package/tests/fixtures/mutation/snapshot-target/README.md +3 -0
  222. package/tests/fixtures/mutation/snapshot-target/state.json +4 -0
  223. package/tests/fixtures/policy/commands.json +164 -0
  224. package/tests/fixtures/policy/properties.json +126 -0
  225. package/tests/fixtures/privacy/cases.json +47 -0
  226. package/tests/fixtures/project-adapter-installation/invalid-adapter-location/.agents/adapters/basic/adapter.json +53 -0
  227. package/tests/fixtures/project-adapter-installation/invalid-adapter-location/.coding-agent/skills.json +23 -0
  228. package/tests/fixtures/project-adapter-installation/invalid-adapter-schema-version/.coding-agent/adapters/basic/adapter.json +53 -0
  229. package/tests/fixtures/project-adapter-installation/invalid-adapter-schema-version/.coding-agent/skills.json +23 -0
  230. package/tests/fixtures/project-adapter-installation/invalid-adapter-version-mismatch/.coding-agent/adapters/basic/adapter.json +53 -0
  231. package/tests/fixtures/project-adapter-installation/invalid-adapter-version-mismatch/.coding-agent/skills.json +23 -0
  232. package/tests/fixtures/project-adapter-installation/invalid-bad-semver/.coding-agent/adapters/basic/adapter.json +53 -0
  233. package/tests/fixtures/project-adapter-installation/invalid-bad-semver/.coding-agent/skills.json +23 -0
  234. package/tests/fixtures/project-adapter-installation/invalid-completion-override/.coding-agent/adapters/basic/adapter.json +53 -0
  235. package/tests/fixtures/project-adapter-installation/invalid-completion-override/.coding-agent/skills.json +23 -0
  236. package/tests/fixtures/project-adapter-installation/invalid-failure-suppression/.coding-agent/adapters/basic/adapter.json +53 -0
  237. package/tests/fixtures/project-adapter-installation/invalid-failure-suppression/.coding-agent/skills.json +23 -0
  238. package/tests/fixtures/project-adapter-installation/invalid-missing-declaration/.coding-agent/adapters/basic/adapter.json +53 -0
  239. package/tests/fixtures/project-adapter-installation/invalid-mode-escalation/.coding-agent/adapters/basic/adapter.json +53 -0
  240. package/tests/fixtures/project-adapter-installation/invalid-mode-escalation/.coding-agent/skills.json +23 -0
  241. package/tests/fixtures/project-adapter-installation/invalid-path-traversal/.coding-agent/adapters/basic/adapter.json +53 -0
  242. package/tests/fixtures/project-adapter-installation/invalid-path-traversal/.coding-agent/skills.json +23 -0
  243. package/tests/fixtures/project-adapter-installation/invalid-scope-expansion/.coding-agent/adapters/basic/adapter.json +53 -0
  244. package/tests/fixtures/project-adapter-installation/invalid-scope-expansion/.coding-agent/skills.json +23 -0
  245. package/tests/fixtures/project-adapter-installation/invalid-secret-exposure/.coding-agent/adapters/basic/adapter.json +53 -0
  246. package/tests/fixtures/project-adapter-installation/invalid-secret-exposure/.coding-agent/skills.json +23 -0
  247. package/tests/fixtures/project-adapter-installation/invalid-skill-mismatch/.coding-agent/adapters/basic/adapter.json +53 -0
  248. package/tests/fixtures/project-adapter-installation/invalid-skill-mismatch/.coding-agent/skills.json +23 -0
  249. package/tests/fixtures/project-adapter-installation/invalid-unknown-skill/.coding-agent/adapters/basic/adapter.json +53 -0
  250. package/tests/fixtures/project-adapter-installation/invalid-unknown-skill/.coding-agent/skills.json +23 -0
  251. package/tests/fixtures/project-adapter-installation/invalid-unsupported-core-version/.coding-agent/adapters/basic/adapter.json +53 -0
  252. package/tests/fixtures/project-adapter-installation/invalid-unsupported-core-version/.coding-agent/skills.json +23 -0
  253. package/tests/fixtures/project-adapter-installation/invalid-weakens-restrictions/.coding-agent/adapters/basic/adapter.json +52 -0
  254. package/tests/fixtures/project-adapter-installation/invalid-weakens-restrictions/.coding-agent/skills.json +23 -0
  255. package/tests/fixtures/project-adapter-installation/valid-compatible-range/coding-agent/adapters/docs/adapter.json +53 -0
  256. package/tests/fixtures/project-adapter-installation/valid-compatible-range/coding-agent.skills.json +23 -0
  257. package/tests/fixtures/project-adapter-installation/valid-exact-pin/.coding-agent/adapters/basic/adapter.json +53 -0
  258. package/tests/fixtures/project-adapter-installation/valid-exact-pin/.coding-agent/skills.json +23 -0
  259. package/tests/fixtures/project-adapter-installation/valid-multiple-adapters/.coding-agent/skills.json +28 -0
  260. package/tests/fixtures/project-adapter-installation/valid-multiple-adapters/adapters/coding-agent/repo/adapter.json +53 -0
  261. package/tests/fixtures/project-adapter-installation/valid-multiple-adapters/adapters/coding-agent/runtime/adapter.json +58 -0
  262. package/tests/fixtures/project-adapter-upgrade-chains/broken-compatibility-chain/01-current/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  263. package/tests/fixtures/project-adapter-upgrade-chains/broken-compatibility-chain/01-current/.coding-agent/skills.json +27 -0
  264. package/tests/fixtures/project-adapter-upgrade-chains/broken-compatibility-chain/02-incompatible/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  265. package/tests/fixtures/project-adapter-upgrade-chains/broken-compatibility-chain/02-incompatible/.coding-agent/skills.json +27 -0
  266. package/tests/fixtures/project-adapter-upgrade-chains/broken-compatibility-chain/03-target/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  267. package/tests/fixtures/project-adapter-upgrade-chains/broken-compatibility-chain/03-target/.coding-agent/skills.json +27 -0
  268. package/tests/fixtures/project-adapter-upgrade-chains/schema-drift-chain/01-current/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  269. package/tests/fixtures/project-adapter-upgrade-chains/schema-drift-chain/01-current/.coding-agent/skills.json +27 -0
  270. package/tests/fixtures/project-adapter-upgrade-chains/schema-drift-chain/02-schema-drift/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  271. package/tests/fixtures/project-adapter-upgrade-chains/schema-drift-chain/02-schema-drift/.coding-agent/skills.json +27 -0
  272. package/tests/fixtures/project-adapter-upgrade-chains/skill-drift-chain/01-current/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  273. package/tests/fixtures/project-adapter-upgrade-chains/skill-drift-chain/01-current/.coding-agent/skills.json +27 -0
  274. package/tests/fixtures/project-adapter-upgrade-chains/skill-drift-chain/02-skill-drift/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  275. package/tests/fixtures/project-adapter-upgrade-chains/skill-drift-chain/02-skill-drift/.coding-agent/skills.json +27 -0
  276. package/tests/fixtures/project-adapter-upgrade-chains/stale-pin-chain/01-current/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  277. package/tests/fixtures/project-adapter-upgrade-chains/stale-pin-chain/01-current/.coding-agent/skills.json +27 -0
  278. package/tests/fixtures/project-adapter-upgrade-chains/stale-pin-chain/02-stale/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  279. package/tests/fixtures/project-adapter-upgrade-chains/stale-pin-chain/02-stale/.coding-agent/skills.json +27 -0
  280. package/tests/fixtures/project-adapter-upgrade-chains/stale-pin-chain/03-target/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  281. package/tests/fixtures/project-adapter-upgrade-chains/stale-pin-chain/03-target/.coding-agent/skills.json +27 -0
  282. package/tests/fixtures/project-adapter-upgrade-chains/unsafe-weakening-chain/01-current/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  283. package/tests/fixtures/project-adapter-upgrade-chains/unsafe-weakening-chain/01-current/.coding-agent/skills.json +27 -0
  284. package/tests/fixtures/project-adapter-upgrade-chains/unsafe-weakening-chain/02-safe/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  285. package/tests/fixtures/project-adapter-upgrade-chains/unsafe-weakening-chain/02-safe/.coding-agent/skills.json +27 -0
  286. package/tests/fixtures/project-adapter-upgrade-chains/unsafe-weakening-chain/03-weakens-restrictions/.coding-agent/adapters/fixture-chain-adapter/adapter.json +69 -0
  287. package/tests/fixtures/project-adapter-upgrade-chains/unsafe-weakening-chain/03-weakens-restrictions/.coding-agent/skills.json +27 -0
  288. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/01-current/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  289. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/01-current/.coding-agent/skills.json +27 -0
  290. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/02-upgrade/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  291. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/02-upgrade/.coding-agent/skills.json +27 -0
  292. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/03-upgrade/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  293. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/03-upgrade/.coding-agent/skills.json +27 -0
  294. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/04-upgrade/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  295. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/04-upgrade/.coding-agent/skills.json +27 -0
  296. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/05-upgrade/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  297. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/05-upgrade/.coding-agent/skills.json +27 -0
  298. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/06-upgrade/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  299. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/06-upgrade/.coding-agent/skills.json +27 -0
  300. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/07-upgrade/.coding-agent/adapters/fixture-chain-adapter/adapter.json +70 -0
  301. package/tests/fixtures/project-adapter-upgrade-chains/valid-chain/07-upgrade/.coding-agent/skills.json +27 -0
  302. package/tests/fixtures/project-adapter-upgrades/adapter-schema-drift/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  303. package/tests/fixtures/project-adapter-upgrades/adapter-schema-drift/after/.coding-agent/skills.json +27 -0
  304. package/tests/fixtures/project-adapter-upgrades/adapter-schema-drift/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  305. package/tests/fixtures/project-adapter-upgrades/adapter-schema-drift/before/.coding-agent/skills.json +27 -0
  306. package/tests/fixtures/project-adapter-upgrades/safe-upgrade-preserves-restrictions/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +71 -0
  307. package/tests/fixtures/project-adapter-upgrades/safe-upgrade-preserves-restrictions/after/.coding-agent/skills.json +27 -0
  308. package/tests/fixtures/project-adapter-upgrades/safe-upgrade-preserves-restrictions/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  309. package/tests/fixtures/project-adapter-upgrades/safe-upgrade-preserves-restrictions/before/.coding-agent/skills.json +27 -0
  310. package/tests/fixtures/project-adapter-upgrades/skill-compatibility-drift/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  311. package/tests/fixtures/project-adapter-upgrades/skill-compatibility-drift/after/.coding-agent/skills.json +27 -0
  312. package/tests/fixtures/project-adapter-upgrades/skill-compatibility-drift/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  313. package/tests/fixtures/project-adapter-upgrades/skill-compatibility-drift/before/.coding-agent/skills.json +27 -0
  314. package/tests/fixtures/project-adapter-upgrades/stale-compatible-range/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  315. package/tests/fixtures/project-adapter-upgrades/stale-compatible-range/after/.coding-agent/skills.json +27 -0
  316. package/tests/fixtures/project-adapter-upgrades/stale-compatible-range/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  317. package/tests/fixtures/project-adapter-upgrades/stale-compatible-range/before/.coding-agent/skills.json +27 -0
  318. package/tests/fixtures/project-adapter-upgrades/stale-exact-pin/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  319. package/tests/fixtures/project-adapter-upgrades/stale-exact-pin/after/.coding-agent/skills.json +27 -0
  320. package/tests/fixtures/project-adapter-upgrades/stale-exact-pin/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  321. package/tests/fixtures/project-adapter-upgrades/stale-exact-pin/before/.coding-agent/skills.json +27 -0
  322. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-mode-escalation/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  323. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-mode-escalation/after/.coding-agent/skills.json +27 -0
  324. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-mode-escalation/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  325. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-mode-escalation/before/.coding-agent/skills.json +27 -0
  326. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-removes-evidence/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +69 -0
  327. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-removes-evidence/after/.coding-agent/skills.json +27 -0
  328. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-removes-evidence/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  329. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-removes-evidence/before/.coding-agent/skills.json +27 -0
  330. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-weakens-restrictions/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +69 -0
  331. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-weakens-restrictions/after/.coding-agent/skills.json +27 -0
  332. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-weakens-restrictions/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  333. package/tests/fixtures/project-adapter-upgrades/unsafe-upgrade-weakens-restrictions/before/.coding-agent/skills.json +27 -0
  334. package/tests/fixtures/project-adapter-upgrades/unsupported-future-core/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  335. package/tests/fixtures/project-adapter-upgrades/unsupported-future-core/after/.coding-agent/skills.json +27 -0
  336. package/tests/fixtures/project-adapter-upgrades/unsupported-future-core/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  337. package/tests/fixtures/project-adapter-upgrades/unsupported-future-core/before/.coding-agent/skills.json +27 -0
  338. package/tests/fixtures/project-adapter-upgrades/unsupported-old-core/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  339. package/tests/fixtures/project-adapter-upgrades/unsupported-old-core/after/.coding-agent/skills.json +27 -0
  340. package/tests/fixtures/project-adapter-upgrades/unsupported-old-core/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  341. package/tests/fixtures/project-adapter-upgrades/unsupported-old-core/before/.coding-agent/skills.json +27 -0
  342. package/tests/fixtures/project-adapter-upgrades/valid-upgrade/after/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  343. package/tests/fixtures/project-adapter-upgrades/valid-upgrade/after/.coding-agent/skills.json +27 -0
  344. package/tests/fixtures/project-adapter-upgrades/valid-upgrade/before/.coding-agent/adapters/fixture-upgrade-adapter/adapter.json +70 -0
  345. package/tests/fixtures/project-adapter-upgrades/valid-upgrade/before/.coding-agent/skills.json +27 -0
  346. package/tests/fixtures/sample-repo/.env.example +1 -0
  347. package/tests/fixtures/sample-repo/README.md +4 -0
  348. package/tests/fixtures/sample-repo/docs/architecture.md +3 -0
  349. package/tests/fixtures/sample-repo/package.json +11 -0
  350. package/tests/fixtures/sample-repo/src/index.js +3 -0
  351. package/tests/fixtures/sample-repo/test/index.test.js +8 -0
  352. package/tests/fixtures/triggers/cases.json +101 -0
  353. package/tests/policy/README.md +16 -0
  354. package/tests/privacy/README.md +14 -0
  355. package/tests/safety/README.md +17 -0
  356. package/tests/trigger/README.md +11 -0
  357. package/work-ledger.md +159 -0
@@ -0,0 +1,2232 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+
8
+ import {
9
+ externalAdapterCliResult,
10
+ formatExternalAdapterSummary,
11
+ validateExternalAdapters,
12
+ } from "./lib/adapter-discovery.mjs";
13
+ import {
14
+ buildEvidenceArchiveReport,
15
+ evidenceArchiveCliResult,
16
+ evidenceBundleCliResult,
17
+ verifyEvidenceBundle,
18
+ } from "./lib/evidence-bundle.mjs";
19
+ import {
20
+ analyzeCommand,
21
+ adapterIssues,
22
+ AUDIT_ONLY_SKILLS,
23
+ auditOnlyDocumentIssues,
24
+ classifyTrigger,
25
+ commandLooksExecutable,
26
+ commandPolicyDecision,
27
+ completionIssues,
28
+ detectSensitiveValues,
29
+ PILOT_SKILLS,
30
+ PILOT_VERSION,
31
+ redactSensitiveText,
32
+ RESTRICTED_CATEGORIES,
33
+ restrictedShellReason,
34
+ } from "./lib/pack-rules.mjs";
35
+ import {
36
+ formatProjectAdapterSummary,
37
+ projectAdapterCliResult,
38
+ validateProjectAdapters,
39
+ } from "./lib/project-adapter-installation.mjs";
40
+ import {
41
+ adapterRepoMapCliResult,
42
+ buildAdapterRepoMapReport,
43
+ renderAdapterRepoMapReport,
44
+ } from "./lib/adapter-repo-map.mjs";
45
+ import {
46
+ adapterUpgradeCliResult,
47
+ checkAdapterUpgrade,
48
+ formatAdapterUpgradeSummary,
49
+ } from "./lib/adapter-upgrade.mjs";
50
+ import {
51
+ adapterChainCliResult,
52
+ checkAdapterUpgradeChain,
53
+ formatAdapterChainSummary,
54
+ } from "./lib/adapter-upgrade-chain.mjs";
55
+ import { validateValue } from "./lib/schema-validator.mjs";
56
+ import { parseSemver, parseVersionPin, satisfiesVersionPin } from "./lib/semver.mjs";
57
+
58
+ const root = path.resolve(process.argv[2] ?? ".");
59
+ const tests = [];
60
+ const requiredSkillFiles = [
61
+ "SKILL.md",
62
+ "checklist.md",
63
+ "examples.md",
64
+ "failure-modes.md",
65
+ "adapter-interface.md",
66
+ "evidence-template.md",
67
+ "agents/openai.yaml",
68
+ ];
69
+ const requiredSkillHeadings = [
70
+ "Purpose And Use",
71
+ "Inputs",
72
+ "Procedure",
73
+ "Evidence, Recovery, And Dependencies",
74
+ "Approval Boundary",
75
+ "Completion",
76
+ ];
77
+ const requiredReleaseFiles = [
78
+ ".github/workflows/validate.yml",
79
+ "AGENTS.md",
80
+ "CHANGELOG.md",
81
+ "CONTRIBUTING.md",
82
+ "RUNBOOK.md",
83
+ "ROADMAP.md",
84
+ "package.json",
85
+ "work-ledger.md",
86
+ "runs/skill-runs.md",
87
+ "bin/coding-agent-skills",
88
+ "scripts/run-next",
89
+ "scripts/validate-maintainer-loop.mjs",
90
+ "docs/versioning/README.md",
91
+ "docs/privacy/README.md",
92
+ "docs/adapters/README.md",
93
+ "docs/usage/README.md",
94
+ "docs/release/README.md",
95
+ "docs/release/npm-package.md",
96
+ "docs/testing/README.md",
97
+ ];
98
+
99
+ function test(name, callback) {
100
+ tests.push({ name, callback });
101
+ }
102
+
103
+ function read(relativePath) {
104
+ return fs.readFileSync(path.join(root, relativePath), "utf8");
105
+ }
106
+
107
+ function readJson(relativePath) {
108
+ return JSON.parse(read(relativePath));
109
+ }
110
+
111
+ function walk(directory, output = []) {
112
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
113
+ if ([".git", "node_modules", "validation-output"].includes(entry.name)) continue;
114
+ const target = path.join(directory, entry.name);
115
+ if (entry.isDirectory()) walk(target, output);
116
+ else output.push(target);
117
+ }
118
+ return output;
119
+ }
120
+
121
+ function assertSchemaValid(schema, value, label) {
122
+ const errors = validateValue(schema, value);
123
+ assert.deepEqual(errors, [], `${label}\n${errors.join("\n")}`);
124
+ }
125
+
126
+ function fencedShellBlocks(text) {
127
+ const blocks = [];
128
+ const pattern = /```(?:bash|sh|shell)\s*\n([\s\S]*?)```/g;
129
+ for (const match of text.matchAll(pattern)) blocks.push(match[1]);
130
+ return blocks;
131
+ }
132
+
133
+ function deepMerge(base, patch) {
134
+ if (
135
+ base === null ||
136
+ patch === null ||
137
+ Array.isArray(base) ||
138
+ Array.isArray(patch) ||
139
+ typeof base !== "object" ||
140
+ typeof patch !== "object"
141
+ ) {
142
+ return structuredClone(patch);
143
+ }
144
+
145
+ const merged = structuredClone(base);
146
+ for (const [key, value] of Object.entries(patch)) {
147
+ merged[key] = Object.hasOwn(merged, key)
148
+ ? deepMerge(merged[key], value)
149
+ : structuredClone(value);
150
+ }
151
+ return merged;
152
+ }
153
+
154
+ function snapshotDirectory(relativePath) {
155
+ const directory = path.join(root, relativePath);
156
+ return snapshotAbsoluteDirectory(directory);
157
+ }
158
+
159
+ function snapshotAbsoluteDirectory(directory) {
160
+ const digest = createHash("sha256");
161
+ for (const file of walk(directory).sort()) {
162
+ digest.update(path.relative(directory, file));
163
+ digest.update(fs.readFileSync(file));
164
+ }
165
+ return digest.digest("hex");
166
+ }
167
+
168
+ const manifestSchema = readJson("schemas/skill-manifest.schema.json");
169
+ const policySchema = readJson("schemas/command-policy.schema.json");
170
+ const adapterSchema = readJson("schemas/project-adapter.schema.json");
171
+ const projectInstallationSchema = readJson(
172
+ "schemas/project-adapter-installation.schema.json",
173
+ );
174
+ const upgradeEvidenceSchema = readJson(
175
+ "schemas/adapter-upgrade-evidence.schema.json",
176
+ );
177
+ const evidenceBundleSchema = readJson("schemas/evidence-bundle.schema.json");
178
+ const evidenceArchiveReportSchema = readJson("schemas/archive-report.schema.json");
179
+ const evidenceArchiveIndexSchema = readJson("schemas/archive-index.schema.json");
180
+ const evidenceSchema = readJson("contracts/evidence-pack/evidence-pack.schema.json");
181
+ const policiesBySkill = Object.fromEntries(
182
+ PILOT_SKILLS.map((skill) => [
183
+ skill,
184
+ readJson(`examples/command-policies/${skill}.json`),
185
+ ]),
186
+ );
187
+
188
+ test("the pilot contains exactly the approved skills", () => {
189
+ const actual = fs
190
+ .readdirSync(path.join(root, "skills"), { withFileTypes: true })
191
+ .filter((entry) => entry.isDirectory())
192
+ .map((entry) => entry.name)
193
+ .sort();
194
+ assert.deepEqual(actual, [...PILOT_SKILLS].sort());
195
+ });
196
+
197
+ test("release governance and safe CI files are present", () => {
198
+ for (const file of requiredReleaseFiles) {
199
+ assert.ok(fs.existsSync(path.join(root, file)), file);
200
+ }
201
+ const ci = read(".github/workflows/validate.yml");
202
+ const runCommands = [...ci.matchAll(/^\s*run:\s*(.+)$/gm)].map((match) => match[1]);
203
+ assert.deepEqual(runCommands, [
204
+ "node scripts/validate-pack.mjs .",
205
+ "node scripts/test-pack.mjs",
206
+ "node scripts/validate-maintainer-loop.mjs .",
207
+ "node scripts/validate-adapters.mjs tests/fixtures/external-adapters/valid-basic",
208
+ "node scripts/validate-project-adapters.mjs tests/fixtures/project-adapter-installation/valid-exact-pin",
209
+ "node scripts/check-adapter-upgrade.mjs tests/fixtures/project-adapter-upgrades/valid-upgrade/before tests/fixtures/project-adapter-upgrades/valid-upgrade/after",
210
+ "node scripts/check-adapter-upgrade-chain.mjs tests/fixtures/project-adapter-upgrade-chains/valid-chain",
211
+ "node scripts/verify-evidence-bundle.mjs tests/fixtures/evidence-bundles/valid-bundle/evidence-bundle.json",
212
+ "node scripts/render-evidence-archive-report.mjs tests/fixtures/evidence-bundles/valid-bundle/evidence-bundle.json",
213
+ "node --test",
214
+ ]);
215
+ });
216
+
217
+ test("the maintainer loop is present, executable, and fails closed", () => {
218
+ const runnerPath = path.join(root, "scripts/run-next");
219
+ const runner = read("scripts/run-next");
220
+ assert.notEqual(fs.statSync(runnerPath).mode & 0o111, 0);
221
+ assert.ok(runner.includes("failClosed"));
222
+ assert.ok(runner.includes("blockedMilestoneReason"));
223
+ assert.ok(runner.includes("work-ledger.md"));
224
+ assert.ok(runner.includes("runs/skill-runs.md"));
225
+ assert.ok(!runner.includes(".env"));
226
+
227
+ for (const permission of [
228
+ "harness-hardening",
229
+ "docs-hardening",
230
+ "test-hardening",
231
+ "adapter-harness",
232
+ "evidence-harness",
233
+ "release-preflight",
234
+ "commit",
235
+ "tag",
236
+ "push",
237
+ ]) {
238
+ assert.ok(runner.includes(permission), permission);
239
+ }
240
+
241
+ for (const args of [[], ["--allow", "unknown-permission"]]) {
242
+ const result = spawnSync(runnerPath, args, {
243
+ cwd: root,
244
+ encoding: "utf8",
245
+ stdio: "pipe",
246
+ });
247
+ assert.notEqual(result.status, 0);
248
+ assert.match(result.stderr, /run-next refused:/);
249
+ }
250
+ });
251
+
252
+ test("local CLI maps approved commands to existing safe scripts", () => {
253
+ const cliPath = path.join(root, "bin", "coding-agent-skills");
254
+ const cliText = read("bin/coding-agent-skills");
255
+ assert.notEqual(fs.statSync(cliPath).mode & 0o111, 0);
256
+ assert.ok(cliText.includes("scripts/validate-pack.mjs"));
257
+ assert.ok(cliText.includes("scripts/validate-project-adapters.mjs"));
258
+ assert.ok(cliText.includes("scripts/render-adapter-repo-map.mjs"));
259
+ assert.ok(cliText.includes("scripts/validate-adapters.mjs"));
260
+ assert.ok(!cliText.includes(".env"));
261
+
262
+ const fixtureRoot = path.join(root, "tests", "fixtures");
263
+ const commands = [
264
+ [["validate-pack"], /pilot pack valid/],
265
+ [
266
+ ["validate-adapters", path.join(fixtureRoot, "external-adapters", "valid-basic")],
267
+ /external adapter validation complete/,
268
+ ],
269
+ [
270
+ [
271
+ "validate-project",
272
+ path.join(fixtureRoot, "project-adapter-installation", "valid-exact-pin"),
273
+ ],
274
+ /project adapter validation complete/,
275
+ ],
276
+ [
277
+ ["repo-map", path.join(fixtureRoot, "project-adapter-installation", "valid-exact-pin")],
278
+ /# Adapter-Aware Repo Map/,
279
+ ],
280
+ ];
281
+
282
+ for (const [args, expected] of commands) {
283
+ const result = spawnSync(cliPath, args, {
284
+ cwd: root,
285
+ encoding: "utf8",
286
+ stdio: "pipe",
287
+ });
288
+ assert.equal(result.status, 0, `${args.join(" ")}\n${result.stderr}`);
289
+ assert.match(result.stdout, expected, args.join(" "));
290
+ }
291
+
292
+ const unknown = spawnSync(cliPath, ["deploy"], {
293
+ cwd: root,
294
+ encoding: "utf8",
295
+ stdio: "pipe",
296
+ });
297
+ assert.equal(unknown.status, 2);
298
+ assert.match(unknown.stderr, /unknown command: deploy/);
299
+ });
300
+
301
+ test("npm package metadata is public-ready and dependency-free", () => {
302
+ const packageJson = readJson("package.json");
303
+ assert.equal(packageJson.name, "coding-agent-skills");
304
+ assert.equal(packageJson.version, "0.2.8");
305
+ assert.equal(
306
+ packageJson.description,
307
+ "Evidence-first, read-only coding-agent skills and project adapter tooling.",
308
+ );
309
+ assert.equal(packageJson.type, "module");
310
+ assert.equal(packageJson.private, false);
311
+ assert.equal(packageJson.license, "MIT");
312
+ assert.deepEqual(packageJson.keywords, [
313
+ "coding-agent",
314
+ "agent-skills",
315
+ "repo-map",
316
+ "project-adapters",
317
+ "code-validation",
318
+ "cli",
319
+ ]);
320
+ assert.deepEqual(packageJson.repository, {
321
+ type: "git",
322
+ url: "git+https://github.com/OneClickPostFactory/coding-agent-skills.git",
323
+ });
324
+ assert.equal(
325
+ packageJson.homepage,
326
+ "https://github.com/OneClickPostFactory/coding-agent-skills#readme",
327
+ );
328
+ assert.deepEqual(packageJson.bugs, {
329
+ url: "https://github.com/OneClickPostFactory/coding-agent-skills/issues",
330
+ });
331
+ assert.deepEqual(packageJson.publishConfig, {
332
+ access: "public",
333
+ registry: "https://registry.npmjs.org/",
334
+ });
335
+ assert.deepEqual(packageJson.bin, {
336
+ "coding-agent-skills": "bin/coding-agent-skills",
337
+ });
338
+ assert.equal(packageJson.dependencies, undefined);
339
+ assert.equal(packageJson.devDependencies, undefined);
340
+ assert.deepEqual(packageJson.files, [
341
+ "bin/",
342
+ "scripts/",
343
+ "skills/",
344
+ "schemas/",
345
+ "contracts/",
346
+ "docs/",
347
+ "examples/",
348
+ "tests/",
349
+ "AGENTS.md",
350
+ "CHANGELOG.md",
351
+ "CONTRIBUTING.md",
352
+ "LICENSE",
353
+ "README.md",
354
+ "ROADMAP.md",
355
+ "RUNBOOK.md",
356
+ "work-ledger.md",
357
+ "runs/skill-runs.md",
358
+ ]);
359
+ assert.equal(packageJson.scripts.validate, "node scripts/validate-pack.mjs .");
360
+ assert.equal(packageJson.scripts["pack:dry-run"], "npm pack --dry-run");
361
+ assert.equal(restrictedShellReason("npm pack --dry-run"), null);
362
+ assert.match(read("LICENSE"), /Copyright \(c\) 2026 OneClickPostFactory/);
363
+ assert.match(read("docs/release/npm-package.md"), /npm install -g coding-agent-skills/);
364
+ });
365
+
366
+ test("validate-pack accepts installed package trees without source-only gitignore", () => {
367
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "installed-package-"));
368
+ const installedRoot = path.join(temporaryRoot, "coding-agent-skills");
369
+
370
+ try {
371
+ fs.cpSync(root, installedRoot, {
372
+ recursive: true,
373
+ filter(source) {
374
+ const relative = path.relative(root, source);
375
+ if (relative === "") return true;
376
+ const parts = relative.split(path.sep);
377
+ return ![
378
+ ".git",
379
+ ".github",
380
+ ".gitignore",
381
+ ".env",
382
+ "node_modules",
383
+ "validation-output",
384
+ ].includes(parts[0]);
385
+ },
386
+ });
387
+
388
+ const result = spawnSync(
389
+ process.execPath,
390
+ [path.join(root, "scripts", "validate-pack.mjs"), installedRoot],
391
+ {
392
+ cwd: root,
393
+ encoding: "utf8",
394
+ stdio: "pipe",
395
+ },
396
+ );
397
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
398
+ assert.match(result.stdout, /pilot pack valid/);
399
+ } finally {
400
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
401
+ }
402
+ });
403
+
404
+ test("every skill has the required files and sections", () => {
405
+ for (const skill of PILOT_SKILLS) {
406
+ for (const file of requiredSkillFiles) {
407
+ assert.ok(fs.existsSync(path.join(root, "skills", skill, file)), `${skill}: ${file}`);
408
+ }
409
+ const skillText = read(`skills/${skill}/SKILL.md`);
410
+ for (const heading of requiredSkillHeadings) {
411
+ assert.ok(skillText.includes(`## ${heading}`), `${skill}: ${heading}`);
412
+ }
413
+ }
414
+ });
415
+
416
+ test("every JSON file parses", () => {
417
+ for (const file of walk(root).filter((candidate) => candidate.endsWith(".json"))) {
418
+ assert.doesNotThrow(
419
+ () => JSON.parse(fs.readFileSync(file, "utf8")),
420
+ path.relative(root, file),
421
+ );
422
+ }
423
+ });
424
+
425
+ test("all manifests, command policies, and evidence examples satisfy their schemas", () => {
426
+ for (const skill of PILOT_SKILLS) {
427
+ assertSchemaValid(
428
+ manifestSchema,
429
+ readJson(`examples/manifests/${skill}.json`),
430
+ `${skill} manifest`,
431
+ );
432
+ assertSchemaValid(
433
+ policySchema,
434
+ readJson(`examples/command-policies/${skill}.json`),
435
+ `${skill} command policy`,
436
+ );
437
+ assertSchemaValid(
438
+ evidenceSchema,
439
+ readJson(`examples/evidence-packs/${skill}.json`),
440
+ `${skill} evidence pack`,
441
+ );
442
+ assert.equal(readJson(`examples/manifests/${skill}.json`).version, PILOT_VERSION);
443
+ assert.equal(
444
+ readJson(`examples/command-policies/${skill}.json`).version,
445
+ PILOT_VERSION,
446
+ );
447
+ assert.equal(
448
+ readJson(`examples/evidence-packs/${skill}.json`).skill.version,
449
+ PILOT_VERSION,
450
+ );
451
+ }
452
+ });
453
+
454
+ test("project adapter examples satisfy schema and compatibility rules", () => {
455
+ const examples = [
456
+ "narrow-repo-map.json",
457
+ "documentation-precedence.json",
458
+ "runtime-status-hints.json",
459
+ ];
460
+
461
+ for (const file of examples) {
462
+ const adapter = readJson(`examples/adapters/${file}`);
463
+ assertSchemaValid(adapterSchema, adapter, file);
464
+ assert.deepEqual(adapterIssues(adapter, { policies: policiesBySkill }), [], file);
465
+ }
466
+ });
467
+
468
+ test("manifest references resolve and agree with skill policy", () => {
469
+ for (const skill of PILOT_SKILLS) {
470
+ const manifestPath = path.join(root, "examples", "manifests", `${skill}.json`);
471
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
472
+ const policyPath = path.resolve(path.dirname(manifestPath), manifest.commandPolicy);
473
+ const evidencePath = path.resolve(path.dirname(manifestPath), manifest.evidenceContract);
474
+ const adapterSchemaPath = path.resolve(path.dirname(manifestPath), manifest.adapterSchema);
475
+ const adapterPath = path.resolve(path.dirname(manifestPath), manifest.adapterInterface);
476
+
477
+ assert.equal(manifest.name, skill);
478
+ assert.ok(fs.existsSync(policyPath), `${skill}: missing command policy`);
479
+ assert.ok(fs.existsSync(evidencePath), `${skill}: missing evidence contract`);
480
+ assert.ok(fs.existsSync(adapterSchemaPath), `${skill}: missing adapter schema`);
481
+ assert.ok(fs.existsSync(adapterPath), `${skill}: missing adapter interface`);
482
+ assert.equal(JSON.parse(fs.readFileSync(policyPath, "utf8")).mode, manifest.mode);
483
+ assert.equal(manifest.adapterCompatibility.contractVersion, "1.0.0");
484
+ assert.ok(manifest.adapterCompatibility.compatibleAdapterVersions.includes("1.0.0"));
485
+ }
486
+ });
487
+
488
+ test("only build-verify is action-capable", () => {
489
+ for (const skill of PILOT_SKILLS) {
490
+ const manifest = readJson(`examples/manifests/${skill}.json`);
491
+ assert.equal(
492
+ manifest.mode,
493
+ skill === "build-verify" ? "action-capable" : "audit-only",
494
+ );
495
+ }
496
+ });
497
+
498
+ test("every command policy preserves all restricted categories", () => {
499
+ for (const skill of PILOT_SKILLS) {
500
+ const policy = readJson(`examples/command-policies/${skill}.json`);
501
+ assert.deepEqual(
502
+ [...new Set(policy.restrictedCategories)].sort(),
503
+ [...RESTRICTED_CATEGORIES].sort(),
504
+ `${skill}: restriction set changed`,
505
+ );
506
+ }
507
+ });
508
+
509
+ test("command policies use explicit constrained families without restricted executables", () => {
510
+ const restrictedExecutables = new Set([
511
+ "npx",
512
+ "wrangler",
513
+ "vercel",
514
+ "netlify",
515
+ "sudo",
516
+ ]);
517
+
518
+ for (const skill of PILOT_SKILLS) {
519
+ const policy = readJson(`examples/command-policies/${skill}.json`);
520
+ const familyNames = policy.allowedFamilies.map((family) => family.name);
521
+ assert.equal(new Set(familyNames).size, familyNames.length, `${skill}: duplicate family`);
522
+ for (const invariant of [
523
+ "inspectEverySegment",
524
+ "inspectScriptBodies",
525
+ "rejectUnknownExecutables",
526
+ "rejectShellWrappers",
527
+ "rejectHeredocs",
528
+ "rejectRedirection",
529
+ "providerSpecificNpx",
530
+ "authenticatedCurlRequiresApproval",
531
+ "boundedReadsRequired",
532
+ ]) {
533
+ assert.equal(policy.parserPolicy[invariant], true, `${skill}: ${invariant}`);
534
+ }
535
+ for (const family of policy.allowedFamilies) {
536
+ assert.ok(family.name.trim(), `${skill}: empty family name`);
537
+ assert.ok(family.executables.length, `${skill}: empty executable list`);
538
+ assert.ok(family.constraints.length, `${skill}: missing constraints`);
539
+ assert.ok(family.argumentPolicy.allowedPatterns.length, `${skill}: allowed patterns`);
540
+ assert.ok(family.argumentPolicy.deniedPatterns.length, `${skill}: denied patterns`);
541
+ for (const executable of family.executables) {
542
+ assert.equal(
543
+ restrictedExecutables.has(executable),
544
+ false,
545
+ `${skill}: restricted executable ${executable}`,
546
+ );
547
+ }
548
+ }
549
+ }
550
+ });
551
+
552
+ test("property-style command-policy cases reject obvious bypass families", () => {
553
+ const fixture = readJson("tests/fixtures/policy/properties.json");
554
+
555
+ for (const candidate of fixture.safeByPolicy) {
556
+ const policy = readJson(`examples/command-policies/${candidate.policy}.json`);
557
+ const result = commandPolicyDecision(candidate.command, policy, {
558
+ scripts: candidate.scripts,
559
+ });
560
+ assert.equal(result.allowed, true, candidate.command);
561
+ assert.equal(result.family, candidate.family, candidate.command);
562
+ }
563
+
564
+ let generated = 0;
565
+ for (const prefix of fixture.safePrefixes) {
566
+ for (const separator of fixture.separators) {
567
+ for (const suffix of fixture.restrictedSuffixes) {
568
+ const result = analyzeCommand(`${prefix}${separator}${suffix.command}`);
569
+ assert.equal(result.allowed, false, `${prefix}${separator}${suffix.command}`);
570
+ assert.match(result.reasons.join("\n"), new RegExp(suffix.reason, "i"));
571
+ generated += 1;
572
+ }
573
+ }
574
+ }
575
+ assert.ok(generated >= 80, `expected broad generated coverage, received ${generated}`);
576
+
577
+ for (const wrapper of fixture.wrappers) {
578
+ for (const suffix of fixture.restrictedSuffixes) {
579
+ const result = analyzeCommand(`${wrapper} '${suffix.command}'`);
580
+ assert.equal(result.allowed, false, `${wrapper}: ${suffix.command}`);
581
+ assert.match(result.reasons.join("\n"), /shell wrapper/i);
582
+ }
583
+ }
584
+ for (const command of fixture.heredocs) {
585
+ assert.match(analyzeCommand(command).reasons.join("\n"), /heredoc/i);
586
+ }
587
+ for (const candidate of fixture.argumentCases) {
588
+ const result = analyzeCommand(candidate.command, {
589
+ approvals: candidate.approvals,
590
+ });
591
+ assert.equal(result.allowed, candidate.allowed, candidate.command);
592
+ if (candidate.reason) {
593
+ assert.match(result.reasons.join("\n"), new RegExp(candidate.reason, "i"));
594
+ }
595
+ }
596
+ for (const candidate of fixture.scriptBodies) {
597
+ const result = analyzeCommand(candidate.command, { scripts: candidate.scripts });
598
+ assert.equal(result.allowed, candidate.allowed, candidate.command);
599
+ if (candidate.reason) {
600
+ assert.match(result.reasons.join("\n"), new RegExp(candidate.reason, "i"));
601
+ }
602
+ }
603
+
604
+ assert.match(analyzeCommand("npx wrangler deploy").reasons.join("\n"), /npx wrangler/i);
605
+ assert.match(analyzeCommand("npx supabase db push").reasons.join("\n"), /npx supabase/i);
606
+ assert.match(analyzeCommand("npx unknown-tool check").reasons.join("\n"), /npx execution/i);
607
+ });
608
+
609
+ test("trigger-classification fixtures select only the intended pilot skill", () => {
610
+ const fixture = readJson("tests/fixtures/triggers/cases.json");
611
+ for (const candidate of fixture.cases) {
612
+ const actual = classifyTrigger(candidate.prompt);
613
+ assert.equal(actual, candidate.expectedSkill, candidate.id);
614
+ for (const excluded of candidate.notSkills ?? []) {
615
+ assert.notEqual(actual, excluded, `${candidate.id}: selected ${excluded}`);
616
+ }
617
+ }
618
+ });
619
+
620
+ test("command-parser fixtures reject obvious policy bypasses", () => {
621
+ const fixture = readJson("tests/fixtures/policy/commands.json");
622
+ for (const candidate of fixture.cases) {
623
+ const result = analyzeCommand(candidate.command, {
624
+ scripts: candidate.scripts,
625
+ });
626
+ assert.equal(result.allowed, candidate.allowed, candidate.id);
627
+ if (candidate.reason) {
628
+ assert.match(
629
+ result.reasons.join("\n"),
630
+ new RegExp(candidate.reason, "i"),
631
+ candidate.id,
632
+ );
633
+ }
634
+ }
635
+ });
636
+
637
+ test("audit-only evidence examples declare no state change", () => {
638
+ for (const skill of AUDIT_ONLY_SKILLS) {
639
+ const evidence = readJson(`examples/evidence-packs/${skill}.json`);
640
+ assert.equal(evidence.changedState.changed, false);
641
+ assert.deepEqual(completionIssues(evidence), []);
642
+ }
643
+ });
644
+
645
+ test("all shipped complete evidence examples are semantically eligible", () => {
646
+ for (const skill of PILOT_SKILLS) {
647
+ const evidence = readJson(`examples/evidence-packs/${skill}.json`);
648
+ assert.equal(evidence.status, "complete");
649
+ assert.deepEqual(completionIssues(evidence), [], skill);
650
+ }
651
+ });
652
+
653
+ test("schema-valid false completion is rejected by semantic policy", () => {
654
+ const evidence = readJson("tests/fixtures/completion/false-complete.json");
655
+ assertSchemaValid(evidenceSchema, evidence, "false-complete fixture");
656
+ assert.match(completionIssues(evidence).join("\n"), /completion-blocking check/);
657
+ });
658
+
659
+ test("false-completion matrix rejects every unsupported complete status", () => {
660
+ const fixture = readJson("tests/fixtures/completion/cases.json");
661
+ for (const candidate of fixture.cases) {
662
+ const evidence = deepMerge(fixture.base, candidate.patch);
663
+ assertSchemaValid(evidenceSchema, evidence, candidate.id);
664
+ const issues = completionIssues(evidence);
665
+ if (candidate.expectedIssue === null) {
666
+ assert.deepEqual(issues, [], candidate.id);
667
+ } else {
668
+ assert.match(issues.join("\n"), new RegExp(candidate.expectedIssue, "i"), candidate.id);
669
+ }
670
+ }
671
+ });
672
+
673
+ test("adapters may extend but may not weaken restrictions", () => {
674
+ const valid = readJson("tests/fixtures/adapters/valid-repo-map.json");
675
+ const weakening = readJson("tests/fixtures/adapters/weakening-repo-map.json");
676
+ assertSchemaValid(adapterSchema, valid, "valid-repo-map");
677
+ assert.deepEqual(adapterIssues(valid, { policies: policiesBySkill }), []);
678
+ assert.ok(
679
+ validateValue(adapterSchema, weakening).length > 0 ||
680
+ adapterIssues(weakening, { policies: policiesBySkill }).some((issue) =>
681
+ issue.includes("weakens"),
682
+ ),
683
+ );
684
+ });
685
+
686
+ test("adapter matrix rejects permission, failure, completion, secret, and mode overrides", () => {
687
+ const valid = readJson("tests/fixtures/adapters/valid-narrowing.json");
688
+ assertSchemaValid(adapterSchema, valid, "valid-narrowing");
689
+ assert.deepEqual(adapterIssues(valid, { policies: policiesBySkill }), []);
690
+
691
+ const invalid = [
692
+ ["allow-deploy.json", /unsafe command alias/],
693
+ ["allow-git-push.json", /unsafe command alias/],
694
+ ["suppress-failures.json", /suppress failures/],
695
+ ["redefine-completion.json", /redefine completion/],
696
+ ["expose-secrets.json", /expose secrets/],
697
+ ["override-audit-only.json", /override runtime-truth mode/],
698
+ ["weakening-repo-map.json", /weakens required restriction/],
699
+ ["incompatible-version.json", /incompatible/],
700
+ ["remove-required-evidence.json", /remove required evidence/],
701
+ ["expand-scope.json", /approval|expand scope/],
702
+ ];
703
+ for (const [file, expected] of invalid) {
704
+ const adapter = readJson(`tests/fixtures/adapters/${file}`);
705
+ const schemaErrors = validateValue(adapterSchema, adapter);
706
+ const semanticErrors = adapterIssues(adapter, { policies: policiesBySkill });
707
+ assert.ok(schemaErrors.length > 0 || semanticErrors.length > 0, file);
708
+ assert.match([...schemaErrors, ...semanticErrors].join("\n"), expected, file);
709
+ }
710
+ });
711
+
712
+ test("external adapter discovery accepts all supported directory conventions", () => {
713
+ const validRoots = [
714
+ ["valid-basic", "repo-map"],
715
+ ["valid-doc-precedence", "llm-drift-control"],
716
+ ["valid-runtime-status", "runtime-truth"],
717
+ ];
718
+
719
+ for (const [fixture, skill] of validRoots) {
720
+ const result = validateExternalAdapters(
721
+ path.join(root, "tests", "fixtures", "external-adapters", fixture),
722
+ { coreRoot: root },
723
+ );
724
+ assert.equal(result.ok, true, fixture);
725
+ assert.equal(result.status, "complete", fixture);
726
+ assert.equal(result.accepted.length, 1, fixture);
727
+ assert.deepEqual(result.accepted[0].skills, [skill], fixture);
728
+ assert.equal(result.rejected.length, 0, fixture);
729
+ assert.equal(result.failures.length, 0, fixture);
730
+ }
731
+ });
732
+
733
+ test("external adapter discovery rejects incompatible and weakening fixtures", () => {
734
+ const invalidRoots = [
735
+ ["invalid-deploy", "unsafe-command-alias"],
736
+ ["invalid-git-push", "unsafe-command-alias"],
737
+ ["invalid-secret-exposure", "secret-exposure"],
738
+ ["invalid-mode-escalation", "mode-override"],
739
+ ["invalid-failure-suppression", "failure-suppression"],
740
+ ["invalid-completion-override", "completion-override"],
741
+ ["invalid-scope-expansion", "scope-expansion"],
742
+ ["invalid-version", "unsupported-adapter-version"],
743
+ ["invalid-skill-id", "unsupported-skill-id"],
744
+ ["invalid-skill-version", "incompatible-skill-version"],
745
+ ["invalid-path-traversal", "unsafe-path"],
746
+ ["invalid-restriction-removal", "restriction-weakening"],
747
+ ["invalid-evidence-suppression", "required-evidence-removal"],
748
+ ["invalid-malformed", "schema-validation"],
749
+ ["invalid-unknown-manifest", "missing-adapter-manifest"],
750
+ ];
751
+
752
+ for (const [fixture, expectedCode] of invalidRoots) {
753
+ const result = validateExternalAdapters(
754
+ path.join(root, "tests", "fixtures", "external-adapters", fixture),
755
+ { coreRoot: root },
756
+ );
757
+ assert.equal(result.ok, false, fixture);
758
+ const codes = [...result.rejected, ...result.failures].flatMap(
759
+ (record) => record.codes,
760
+ );
761
+ assert.ok(codes.includes(expectedCode), `${fixture}: ${codes.join(",")}`);
762
+ }
763
+ });
764
+
765
+ test("external adapter discovery handles mixed, empty, missing, and traversal roots", () => {
766
+ const fixtureRoot = path.join(root, "tests", "fixtures", "external-adapters");
767
+ const mixed = validateExternalAdapters(path.join(fixtureRoot, "mixed"), {
768
+ coreRoot: root,
769
+ });
770
+ assert.equal(mixed.ok, false);
771
+ assert.equal(mixed.accepted.length, 1);
772
+ assert.equal(mixed.rejected.length, 1);
773
+
774
+ const empty = validateExternalAdapters(path.join(fixtureRoot, "empty"), {
775
+ coreRoot: root,
776
+ });
777
+ assert.equal(empty.ok, true);
778
+ assert.equal(empty.status, "empty");
779
+ assert.equal(empty.discovered, 0);
780
+
781
+ const missing = validateExternalAdapters(path.join(fixtureRoot, "missing"), {
782
+ coreRoot: root,
783
+ });
784
+ assert.equal(missing.ok, false);
785
+ assert.deepEqual(missing.failures[0].codes, ["adapter-root-not-found"]);
786
+
787
+ const traversal = validateExternalAdapters("../external-adapters/valid-basic", {
788
+ coreRoot: root,
789
+ });
790
+ assert.equal(traversal.ok, false);
791
+ assert.deepEqual(traversal.failures[0].codes, ["root-path-traversal"]);
792
+ });
793
+
794
+ test("external adapter discovery rejects malformed JSON and symlink escapes", () => {
795
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "adapter-discovery-"));
796
+ try {
797
+ const malformedDirectory = path.join(
798
+ temporaryRoot,
799
+ "malformed",
800
+ ".coding-agent",
801
+ "adapters",
802
+ "sample",
803
+ );
804
+ fs.mkdirSync(malformedDirectory, { recursive: true });
805
+ fs.copyFileSync(
806
+ path.join(
807
+ root,
808
+ "tests",
809
+ "fixtures",
810
+ "external-adapters",
811
+ "invalid-malformed",
812
+ "malformed-adapter.txt",
813
+ ),
814
+ path.join(malformedDirectory, "adapter.json"),
815
+ );
816
+ const malformed = validateExternalAdapters(
817
+ path.join(temporaryRoot, "malformed"),
818
+ { coreRoot: root },
819
+ );
820
+ assert.equal(malformed.ok, false);
821
+ assert.deepEqual(malformed.rejected[0].codes, ["malformed-json"]);
822
+
823
+ const symlinkRoot = path.join(temporaryRoot, "symlink");
824
+ fs.mkdirSync(path.join(symlinkRoot, ".coding-agent"), { recursive: true });
825
+ fs.symlinkSync(
826
+ path.join(
827
+ root,
828
+ "tests",
829
+ "fixtures",
830
+ "external-adapters",
831
+ "valid-basic",
832
+ ".coding-agent",
833
+ "adapters",
834
+ ),
835
+ path.join(symlinkRoot, ".coding-agent", "adapters"),
836
+ "dir",
837
+ );
838
+ const symlink = validateExternalAdapters(symlinkRoot, { coreRoot: root });
839
+ assert.equal(symlink.ok, false);
840
+ assert.deepEqual(symlink.failures[0].codes, ["symlink-escape"]);
841
+ } finally {
842
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
843
+ }
844
+ });
845
+
846
+ test("external adapter discovery ignores unrelated secret files and redacts manifest rejection", () => {
847
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "adapter-privacy-"));
848
+ const syntheticValue = readJson("tests/fixtures/privacy/cases.json")
849
+ .cases.find((candidate) => candidate.id === "fake-github-token")
850
+ .parts.join("");
851
+
852
+ try {
853
+ const safeRoot = path.join(temporaryRoot, "safe");
854
+ const safeDirectory = path.join(
855
+ safeRoot,
856
+ ".coding-agent",
857
+ "adapters",
858
+ "sample",
859
+ );
860
+ fs.mkdirSync(safeDirectory, { recursive: true });
861
+ fs.copyFileSync(
862
+ path.join(
863
+ root,
864
+ "tests",
865
+ "fixtures",
866
+ "external-adapters",
867
+ "valid-basic",
868
+ ".coding-agent",
869
+ "adapters",
870
+ "basic",
871
+ "adapter.json",
872
+ ),
873
+ path.join(safeDirectory, "adapter.json"),
874
+ );
875
+ fs.writeFileSync(path.join(safeRoot, ".env"), `SYNTHETIC=${syntheticValue}\n`);
876
+ const safe = validateExternalAdapters(safeRoot, { coreRoot: root });
877
+ assert.equal(safe.ok, true);
878
+
879
+ const rejectedRoot = path.join(temporaryRoot, "rejected");
880
+ const rejectedDirectory = path.join(
881
+ rejectedRoot,
882
+ ".coding-agent",
883
+ "adapters",
884
+ "sample",
885
+ );
886
+ fs.mkdirSync(rejectedDirectory, { recursive: true });
887
+ fs.writeFileSync(
888
+ path.join(rejectedDirectory, "adapter.json"),
889
+ JSON.stringify({ synthetic: syntheticValue }),
890
+ );
891
+ const rejected = validateExternalAdapters(rejectedRoot, { coreRoot: root });
892
+ assert.equal(rejected.ok, false);
893
+ assert.deepEqual(rejected.rejected[0].codes, ["secret-like-content"]);
894
+ assert.doesNotMatch(
895
+ formatExternalAdapterSummary(rejected).join("\n"),
896
+ new RegExp(syntheticValue),
897
+ );
898
+ } finally {
899
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
900
+ }
901
+ });
902
+
903
+ test("external adapter CLI uses stable exit codes and safe summaries", () => {
904
+ const fixtureRoot = path.join(root, "tests", "fixtures", "external-adapters");
905
+ const valid = externalAdapterCliResult(path.join(fixtureRoot, "valid-basic"), {
906
+ coreRoot: root,
907
+ });
908
+ assert.equal(valid.exitCode, 0);
909
+ assert.equal(valid.stream, "stdout");
910
+ assert.match(valid.lines.join("\n"), /1 accepted, 0 rejected/);
911
+
912
+ const invalid = externalAdapterCliResult(
913
+ path.join(fixtureRoot, "invalid-deploy"),
914
+ { coreRoot: root },
915
+ );
916
+ assert.equal(invalid.exitCode, 1);
917
+ assert.equal(invalid.stream, "stderr");
918
+ assert.match(invalid.lines.join("\n"), /unsafe-command-alias/);
919
+ assert.doesNotMatch(
920
+ invalid.lines.join("\n"),
921
+ /wrangler|fixture-external|adapterId/i,
922
+ );
923
+
924
+ const usage = externalAdapterCliResult(undefined, {
925
+ coreRoot: root,
926
+ });
927
+ assert.equal(usage.exitCode, 2);
928
+ assert.equal(usage.stream, "stderr");
929
+ assert.match(usage.lines.join("\n"), /usage:/i);
930
+
931
+ const summary = formatExternalAdapterSummary(
932
+ validateExternalAdapters(path.join(fixtureRoot, "mixed"), { coreRoot: root }),
933
+ ).join("\n");
934
+ assert.doesNotMatch(summary, /git push|fixture-mixed|adapterId/i);
935
+ });
936
+
937
+ test("project adapter declarations satisfy schema and supported pin forms", () => {
938
+ const fixtureRoot = path.join(
939
+ root,
940
+ "tests",
941
+ "fixtures",
942
+ "project-adapter-installation",
943
+ );
944
+ const declarations = [
945
+ ["valid-exact-pin", ".coding-agent/skills.json"],
946
+ ["valid-compatible-range", "coding-agent.skills.json"],
947
+ ["valid-multiple-adapters", ".coding-agent/skills.json"],
948
+ ];
949
+
950
+ for (const [fixture, relative] of declarations) {
951
+ assertSchemaValid(
952
+ projectInstallationSchema,
953
+ JSON.parse(fs.readFileSync(path.join(fixtureRoot, fixture, relative), "utf8")),
954
+ fixture,
955
+ );
956
+ }
957
+
958
+ assert.deepEqual(parseSemver("0.1.6"), [0, 1, 6]);
959
+ assert.equal(parseSemver("v0.1.6"), null);
960
+ assert.equal(parseSemver("00.1.6"), null);
961
+ assert.ok(parseVersionPin("0.1.6"));
962
+ assert.ok(parseVersionPin(">=0.1.3 <0.2.0"));
963
+ assert.equal(parseVersionPin("^0.1.6"), null);
964
+ assert.equal(satisfiesVersionPin("0.1.6", "0.1.6"), true);
965
+ assert.equal(satisfiesVersionPin("0.1.6", ">=0.1.3 <0.2.0"), true);
966
+ assert.equal(satisfiesVersionPin("0.1.6", "<0.1.6"), false);
967
+ assert.equal(satisfiesVersionPin("0.1.6", ">=0.2.0"), false);
968
+ });
969
+
970
+ test("project adapter installation accepts exact, range, and multiple adapters", () => {
971
+ const fixtureRoot = path.join(
972
+ root,
973
+ "tests",
974
+ "fixtures",
975
+ "project-adapter-installation",
976
+ );
977
+ const valid = [
978
+ ["valid-exact-pin", 1, ["repo-map"]],
979
+ ["valid-compatible-range", 1, ["llm-drift-control"]],
980
+ ["valid-multiple-adapters", 2, ["repo-map", "runtime-truth"]],
981
+ ];
982
+
983
+ for (const [fixture, adapterCount, skills] of valid) {
984
+ const result = validateProjectAdapters(path.join(fixtureRoot, fixture), {
985
+ coreRoot: root,
986
+ });
987
+ assert.equal(result.ok, true, fixture);
988
+ assert.equal(result.acceptedAdapters, adapterCount, fixture);
989
+ assert.deepEqual(result.acceptedSkills, skills, fixture);
990
+ }
991
+ });
992
+
993
+ test("project adapter installation rejects invalid pins, declarations, and adapters", () => {
994
+ const fixtureRoot = path.join(
995
+ root,
996
+ "tests",
997
+ "fixtures",
998
+ "project-adapter-installation",
999
+ );
1000
+ const invalid = [
1001
+ ["invalid-missing-declaration", "missing-project-declaration"],
1002
+ ["invalid-unsupported-core-version", "unsupported-core-version"],
1003
+ ["invalid-bad-semver", "invalid-semver"],
1004
+ ["invalid-unknown-skill", "unsupported-skill-id"],
1005
+ ["invalid-adapter-version-mismatch", "adapter-version-mismatch"],
1006
+ ["invalid-adapter-schema-version", "unsupported-adapter-version"],
1007
+ ["invalid-adapter-location", "invalid-adapter-location"],
1008
+ ["invalid-skill-mismatch", "adapter-skill-mismatch"],
1009
+ ["invalid-mode-escalation", "mode-override"],
1010
+ ["invalid-failure-suppression", "failure-suppression"],
1011
+ ["invalid-completion-override", "completion-override"],
1012
+ ["invalid-weakens-restrictions", "restriction-weakening"],
1013
+ ["invalid-secret-exposure", "secret-exposure"],
1014
+ ["invalid-scope-expansion", "scope-expansion"],
1015
+ ["invalid-path-traversal", "unsafe-project-path"],
1016
+ ];
1017
+
1018
+ for (const [fixture, expectedCode] of invalid) {
1019
+ const result = validateProjectAdapters(path.join(fixtureRoot, fixture), {
1020
+ coreRoot: root,
1021
+ });
1022
+ assert.equal(result.ok, false, fixture);
1023
+ assert.ok(result.codes.includes(expectedCode), `${fixture}: ${result.codes}`);
1024
+ }
1025
+ });
1026
+
1027
+ test("project adapter installation rejects old core pins, ambiguity, and symlink escape", () => {
1028
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "project-adapter-"));
1029
+ const source = path.join(
1030
+ root,
1031
+ "tests",
1032
+ "fixtures",
1033
+ "project-adapter-installation",
1034
+ "valid-exact-pin",
1035
+ );
1036
+
1037
+ try {
1038
+ const oldCore = path.join(temporaryRoot, "old-core");
1039
+ fs.cpSync(source, oldCore, { recursive: true });
1040
+ const oldDeclarationPath = path.join(oldCore, ".coding-agent", "skills.json");
1041
+ const oldDeclaration = JSON.parse(fs.readFileSync(oldDeclarationPath, "utf8"));
1042
+ oldDeclaration.core.expectedVersion = "0.1.0";
1043
+ oldDeclaration.core.versionPin = "0.1.0";
1044
+ fs.writeFileSync(oldDeclarationPath, JSON.stringify(oldDeclaration));
1045
+ const oldResult = validateProjectAdapters(oldCore, { coreRoot: root });
1046
+ assert.equal(oldResult.ok, false);
1047
+ assert.ok(oldResult.codes.includes("unsupported-core-version"));
1048
+
1049
+ const missingVersion = path.join(temporaryRoot, "missing-version");
1050
+ fs.cpSync(source, missingVersion, { recursive: true });
1051
+ const missingVersionPath = path.join(
1052
+ missingVersion,
1053
+ ".coding-agent",
1054
+ "skills.json",
1055
+ );
1056
+ const missingDeclaration = JSON.parse(
1057
+ fs.readFileSync(missingVersionPath, "utf8"),
1058
+ );
1059
+ delete missingDeclaration.core.versionPin;
1060
+ fs.writeFileSync(missingVersionPath, JSON.stringify(missingDeclaration));
1061
+ const missingVersionResult = validateProjectAdapters(missingVersion, {
1062
+ coreRoot: root,
1063
+ });
1064
+ assert.equal(missingVersionResult.ok, false);
1065
+ assert.ok(missingVersionResult.codes.includes("declaration-schema"));
1066
+ assert.ok(missingVersionResult.codes.includes("invalid-semver"));
1067
+
1068
+ const ambiguous = path.join(temporaryRoot, "ambiguous");
1069
+ fs.cpSync(source, ambiguous, { recursive: true });
1070
+ fs.copyFileSync(
1071
+ path.join(ambiguous, ".coding-agent", "skills.json"),
1072
+ path.join(ambiguous, "coding-agent.skills.json"),
1073
+ );
1074
+ const ambiguousResult = validateProjectAdapters(ambiguous, { coreRoot: root });
1075
+ assert.equal(ambiguousResult.ok, false);
1076
+ assert.deepEqual(ambiguousResult.codes, ["ambiguous-project-declaration"]);
1077
+
1078
+ const symlinkRoot = path.join(temporaryRoot, "symlink");
1079
+ fs.mkdirSync(path.join(symlinkRoot, ".coding-agent"), { recursive: true });
1080
+ fs.symlinkSync(
1081
+ path.join(source, ".coding-agent", "skills.json"),
1082
+ path.join(symlinkRoot, ".coding-agent", "skills.json"),
1083
+ );
1084
+ const symlinkResult = validateProjectAdapters(symlinkRoot, { coreRoot: root });
1085
+ assert.equal(symlinkResult.ok, false);
1086
+ assert.deepEqual(symlinkResult.codes, ["symlink-escape"]);
1087
+ } finally {
1088
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1089
+ }
1090
+ });
1091
+
1092
+ test("project adapter installation ignores .env and keeps summaries secret-safe", () => {
1093
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "project-privacy-"));
1094
+ const source = path.join(
1095
+ root,
1096
+ "tests",
1097
+ "fixtures",
1098
+ "project-adapter-installation",
1099
+ "valid-exact-pin",
1100
+ );
1101
+ const syntheticValue = readJson("tests/fixtures/privacy/cases.json")
1102
+ .cases.find((candidate) => candidate.id === "fake-github-token")
1103
+ .parts.join("");
1104
+
1105
+ try {
1106
+ const safe = path.join(temporaryRoot, "safe");
1107
+ fs.cpSync(source, safe, { recursive: true });
1108
+ fs.writeFileSync(path.join(safe, ".env"), `SYNTHETIC=${syntheticValue}\n`);
1109
+ assert.equal(validateProjectAdapters(safe, { coreRoot: root }).ok, true);
1110
+
1111
+ const rejected = path.join(temporaryRoot, "rejected");
1112
+ fs.cpSync(source, rejected, { recursive: true });
1113
+ const declarationPath = path.join(rejected, ".coding-agent", "skills.json");
1114
+ const declaration = JSON.parse(fs.readFileSync(declarationPath, "utf8"));
1115
+ declaration.syntheticNote = syntheticValue;
1116
+ fs.writeFileSync(declarationPath, JSON.stringify(declaration));
1117
+ const rejectedResult = validateProjectAdapters(rejected, { coreRoot: root });
1118
+ assert.equal(rejectedResult.ok, false);
1119
+ assert.deepEqual(rejectedResult.codes, ["secret-like-content"]);
1120
+ assert.doesNotMatch(
1121
+ formatProjectAdapterSummary(rejectedResult).join("\n"),
1122
+ new RegExp(syntheticValue),
1123
+ );
1124
+ } finally {
1125
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1126
+ }
1127
+ });
1128
+
1129
+ test("project adapter CLI uses stable exit codes and safe summaries", () => {
1130
+ const fixtureRoot = path.join(
1131
+ root,
1132
+ "tests",
1133
+ "fixtures",
1134
+ "project-adapter-installation",
1135
+ );
1136
+ const valid = projectAdapterCliResult(path.join(fixtureRoot, "valid-exact-pin"), {
1137
+ coreRoot: root,
1138
+ });
1139
+ assert.equal(valid.exitCode, 0);
1140
+ assert.equal(valid.stream, "stdout");
1141
+ assert.match(valid.lines.join("\n"), /core pin accepted/);
1142
+
1143
+ const invalid = projectAdapterCliResult(
1144
+ path.join(fixtureRoot, "invalid-secret-exposure"),
1145
+ { coreRoot: root },
1146
+ );
1147
+ assert.equal(invalid.exitCode, 1);
1148
+ assert.equal(invalid.stream, "stderr");
1149
+ assert.match(invalid.lines.join("\n"), /secret-exposure/);
1150
+ assert.doesNotMatch(invalid.lines.join("\n"), /fixture-project|adapterId/i);
1151
+
1152
+ const usage = projectAdapterCliResult(undefined, { coreRoot: root });
1153
+ assert.equal(usage.exitCode, 2);
1154
+ assert.equal(usage.stream, "stderr");
1155
+ assert.match(usage.lines.join("\n"), /usage:/i);
1156
+ });
1157
+
1158
+ test("adapter-aware repo-map consumes validated project adapter metadata", () => {
1159
+ const fixtureRoot = path.join(
1160
+ root,
1161
+ "tests",
1162
+ "fixtures",
1163
+ "project-adapter-installation",
1164
+ );
1165
+ const report = buildAdapterRepoMapReport(path.join(fixtureRoot, "valid-exact-pin"), {
1166
+ coreRoot: root,
1167
+ });
1168
+ assert.equal(report.ok, true, report.codes?.join(","));
1169
+ assert.deepEqual(report.enabledSkills, ["repo-map"]);
1170
+ assert.deepEqual(report.adapterIds, ["fixture-project-basic"]);
1171
+ assert.deepEqual(
1172
+ report.safeReadPaths.map((record) => record.path),
1173
+ ["README.md", "src"],
1174
+ );
1175
+ assert.deepEqual(report.ignoredPaths, ["dist"]);
1176
+ assert.deepEqual(report.requiredEvidence, [
1177
+ "repository root",
1178
+ "application entry point",
1179
+ ]);
1180
+
1181
+ const rendered = renderAdapterRepoMapReport(report);
1182
+ assert.match(rendered, /# Adapter-Aware Repo Map/);
1183
+ assert.match(rendered, /## Safe Read Paths/);
1184
+ assert.match(rendered, /README\.md/);
1185
+ assert.match(rendered, /## Ignored Paths/);
1186
+ assert.match(rendered, /No target project build, test, runtime, deployment/);
1187
+ });
1188
+
1189
+ test("adapter-aware repo-map fails closed without repo-map compatibility", () => {
1190
+ const fixtureRoot = path.join(
1191
+ root,
1192
+ "tests",
1193
+ "fixtures",
1194
+ "project-adapter-installation",
1195
+ );
1196
+ const report = buildAdapterRepoMapReport(
1197
+ path.join(fixtureRoot, "valid-compatible-range"),
1198
+ { coreRoot: root },
1199
+ );
1200
+ assert.equal(report.ok, false);
1201
+ assert.deepEqual(report.codes, ["repo-map-not-enabled"]);
1202
+
1203
+ const cli = adapterRepoMapCliResult(path.join(fixtureRoot, "valid-exact-pin"), {
1204
+ coreRoot: root,
1205
+ });
1206
+ assert.equal(cli.exitCode, 0);
1207
+ assert.equal(cli.stream, "stdout");
1208
+ assert.match(cli.lines.join("\n"), /Enabled skills: repo-map/);
1209
+
1210
+ const usage = adapterRepoMapCliResult(undefined, { coreRoot: root });
1211
+ assert.equal(usage.exitCode, 2);
1212
+ assert.equal(usage.stream, "stderr");
1213
+ assert.match(usage.lines.join("\n"), /usage:/i);
1214
+ });
1215
+
1216
+ test("adapter upgrade accepts safe exact and compatible-range revisions", () => {
1217
+ const fixtureRoot = path.join(
1218
+ root,
1219
+ "tests",
1220
+ "fixtures",
1221
+ "project-adapter-upgrades",
1222
+ );
1223
+ for (const fixture of ["valid-upgrade", "safe-upgrade-preserves-restrictions"]) {
1224
+ const result = checkAdapterUpgrade(
1225
+ path.join(fixtureRoot, fixture, "before"),
1226
+ path.join(fixtureRoot, fixture, "after"),
1227
+ { coreRoot: root },
1228
+ );
1229
+ assert.equal(result.ok, true, `${fixture}: ${result.codes}`);
1230
+ assert.equal(result.comparedAdapters, 1);
1231
+ assert.equal(result.comparedSkills, 1);
1232
+ }
1233
+ });
1234
+
1235
+ test("adapter upgrade detects stale exact pins and compatible ranges", () => {
1236
+ const fixtureRoot = path.join(
1237
+ root,
1238
+ "tests",
1239
+ "fixtures",
1240
+ "project-adapter-upgrades",
1241
+ );
1242
+ for (const [fixture, code] of [
1243
+ ["stale-exact-pin", "stale-exact-pin"],
1244
+ ["stale-compatible-range", "stale-compatible-range"],
1245
+ ]) {
1246
+ const result = checkAdapterUpgrade(
1247
+ path.join(fixtureRoot, fixture, "before"),
1248
+ path.join(fixtureRoot, fixture, "after"),
1249
+ { coreRoot: root },
1250
+ );
1251
+ assert.equal(result.ok, false, fixture);
1252
+ assert.ok(result.codes.includes(code), `${fixture}: ${result.codes}`);
1253
+ }
1254
+ });
1255
+
1256
+ test("adapter upgrade rejects unsupported cores and compatibility drift", () => {
1257
+ const fixtureRoot = path.join(
1258
+ root,
1259
+ "tests",
1260
+ "fixtures",
1261
+ "project-adapter-upgrades",
1262
+ );
1263
+ for (const [fixture, code] of [
1264
+ ["unsupported-future-core", "unsupported-future-core"],
1265
+ ["unsupported-old-core", "unsupported-old-core"],
1266
+ ["adapter-schema-drift", "adapter-schema-drift"],
1267
+ ["skill-compatibility-drift", "skill-compatibility-drift"],
1268
+ ]) {
1269
+ const result = checkAdapterUpgrade(
1270
+ path.join(fixtureRoot, fixture, "before"),
1271
+ path.join(fixtureRoot, fixture, "after"),
1272
+ { coreRoot: root },
1273
+ );
1274
+ assert.equal(result.ok, false, fixture);
1275
+ assert.ok(result.codes.includes(code), `${fixture}: ${result.codes}`);
1276
+ }
1277
+ });
1278
+
1279
+ test("adapter upgrade rejects restriction, mode, and evidence weakening", () => {
1280
+ const fixtureRoot = path.join(
1281
+ root,
1282
+ "tests",
1283
+ "fixtures",
1284
+ "project-adapter-upgrades",
1285
+ );
1286
+ for (const [fixture, code] of [
1287
+ ["unsafe-upgrade-weakens-restrictions", "restriction-weakening"],
1288
+ ["unsafe-upgrade-mode-escalation", "mode-escalation"],
1289
+ ["unsafe-upgrade-removes-evidence", "required-evidence-removal"],
1290
+ ]) {
1291
+ const result = checkAdapterUpgrade(
1292
+ path.join(fixtureRoot, fixture, "before"),
1293
+ path.join(fixtureRoot, fixture, "after"),
1294
+ { coreRoot: root },
1295
+ );
1296
+ assert.equal(result.ok, false, fixture);
1297
+ assert.ok(result.codes.includes(code), `${fixture}: ${result.codes}`);
1298
+ }
1299
+ });
1300
+
1301
+ test("adapter upgrade rejects dynamic unsafe revision attempts without leaking values", () => {
1302
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "adapter-upgrade-"));
1303
+ const source = path.join(
1304
+ root,
1305
+ "tests",
1306
+ "fixtures",
1307
+ "project-adapter-upgrades",
1308
+ "valid-upgrade",
1309
+ );
1310
+ const syntheticValue = readJson("tests/fixtures/privacy/cases.json")
1311
+ .cases.find((candidate) => candidate.id === "fake-github-token")
1312
+ .parts.join("");
1313
+
1314
+ function prepare(name) {
1315
+ const destination = path.join(temporaryRoot, name);
1316
+ fs.cpSync(source, destination, { recursive: true });
1317
+ return {
1318
+ before: path.join(destination, "before"),
1319
+ after: path.join(destination, "after"),
1320
+ declaration: path.join(
1321
+ destination,
1322
+ "after",
1323
+ ".coding-agent",
1324
+ "skills.json",
1325
+ ),
1326
+ adapter: path.join(
1327
+ destination,
1328
+ "after",
1329
+ ".coding-agent",
1330
+ "adapters",
1331
+ "fixture-upgrade-adapter",
1332
+ "adapter.json",
1333
+ ),
1334
+ };
1335
+ }
1336
+
1337
+ function editJson(file, callback) {
1338
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
1339
+ callback(value);
1340
+ fs.writeFileSync(file, JSON.stringify(value));
1341
+ }
1342
+
1343
+ try {
1344
+ for (const [name, code, mutate] of [
1345
+ [
1346
+ "failure",
1347
+ "failure-suppression",
1348
+ ({ adapter }) =>
1349
+ editJson(adapter, (value) => {
1350
+ value.inheritance.allowFailureSuppression = true;
1351
+ }),
1352
+ ],
1353
+ [
1354
+ "completion",
1355
+ "completion-override",
1356
+ ({ adapter }) =>
1357
+ editJson(adapter, (value) => {
1358
+ value.inheritance.allowCompletionOverride = true;
1359
+ }),
1360
+ ],
1361
+ [
1362
+ "adapter-version",
1363
+ "adapter-version-drift",
1364
+ ({ declaration, adapter }) => {
1365
+ editJson(declaration, (value) => {
1366
+ value.adapters[0].version = "1.0.1";
1367
+ });
1368
+ editJson(adapter, (value) => {
1369
+ value.adapterVersion = "1.0.1";
1370
+ });
1371
+ },
1372
+ ],
1373
+ [
1374
+ "unknown-skill",
1375
+ "unknown-skill-compatibility",
1376
+ ({ declaration, adapter }) => {
1377
+ editJson(declaration, (value) => {
1378
+ value.compatibleSkillIds = ["future-skill"];
1379
+ value.adapters[0].skillIds = ["future-skill"];
1380
+ });
1381
+ editJson(adapter, (value) => {
1382
+ value.supportedSkills[0].id = "future-skill";
1383
+ });
1384
+ },
1385
+ ],
1386
+ [
1387
+ "path",
1388
+ "path-traversal",
1389
+ ({ declaration }) =>
1390
+ editJson(declaration, (value) => {
1391
+ value.evidenceOutput = "../outside/evidence.json";
1392
+ }),
1393
+ ],
1394
+ [
1395
+ "scope",
1396
+ "scope-expansion",
1397
+ ({ adapter }) =>
1398
+ editJson(adapter, (value) => {
1399
+ value.project.detection.requireApprovalOutsideScope = false;
1400
+ value.inheritance.allowScopeExpansionWithoutApproval = true;
1401
+ }),
1402
+ ],
1403
+ ]) {
1404
+ const revision = prepare(name);
1405
+ mutate(revision);
1406
+ const result = checkAdapterUpgrade(revision.before, revision.after, {
1407
+ coreRoot: root,
1408
+ });
1409
+ assert.equal(result.ok, false, name);
1410
+ assert.ok(result.codes.includes(code), `${name}: ${result.codes}`);
1411
+ }
1412
+
1413
+ const env = prepare("env");
1414
+ fs.writeFileSync(path.join(env.after, ".env"), `SYNTHETIC=${syntheticValue}\n`);
1415
+ const envResult = checkAdapterUpgrade(env.before, env.after, {
1416
+ coreRoot: root,
1417
+ });
1418
+ assert.equal(envResult.ok, true, envResult.codes.join(","));
1419
+
1420
+ const secret = prepare("secret");
1421
+ editJson(secret.declaration, (value) => {
1422
+ value.syntheticNote = syntheticValue;
1423
+ });
1424
+ const secretResult = checkAdapterUpgrade(secret.before, secret.after, {
1425
+ coreRoot: root,
1426
+ });
1427
+ assert.equal(secretResult.ok, false);
1428
+ assert.ok(secretResult.codes.includes("secret-exposure"));
1429
+ assert.doesNotMatch(
1430
+ formatAdapterUpgradeSummary(secretResult).join("\n"),
1431
+ new RegExp(syntheticValue),
1432
+ );
1433
+ } finally {
1434
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1435
+ }
1436
+ });
1437
+
1438
+ test("adapter upgrade CLI uses stable exit codes and safe summaries", () => {
1439
+ const fixtureRoot = path.join(
1440
+ root,
1441
+ "tests",
1442
+ "fixtures",
1443
+ "project-adapter-upgrades",
1444
+ );
1445
+ const valid = adapterUpgradeCliResult(
1446
+ path.join(fixtureRoot, "valid-upgrade", "before"),
1447
+ path.join(fixtureRoot, "valid-upgrade", "after"),
1448
+ { coreRoot: root },
1449
+ );
1450
+ assert.equal(valid.exitCode, 0);
1451
+ assert.equal(valid.stream, "stdout");
1452
+ assert.match(valid.lines.join("\n"), /target core accepted/);
1453
+
1454
+ const invalid = adapterUpgradeCliResult(
1455
+ path.join(fixtureRoot, "stale-exact-pin", "before"),
1456
+ path.join(fixtureRoot, "stale-exact-pin", "after"),
1457
+ { coreRoot: root },
1458
+ );
1459
+ assert.equal(invalid.exitCode, 1);
1460
+ assert.equal(invalid.stream, "stderr");
1461
+ assert.match(invalid.lines.join("\n"), /stale-exact-pin/);
1462
+ assert.doesNotMatch(invalid.lines.join("\n"), /fixture-upgrade|adapterId/i);
1463
+
1464
+ const usage = adapterUpgradeCliResult(undefined, undefined, {
1465
+ coreRoot: root,
1466
+ });
1467
+ assert.equal(usage.exitCode, 2);
1468
+ assert.equal(usage.stream, "stderr");
1469
+ assert.match(usage.lines.join("\n"), /usage:/i);
1470
+ });
1471
+
1472
+ test("upgrade evidence examples validate and declare no project state change", () => {
1473
+ for (const file of [
1474
+ "valid-upgrade.evidence.json",
1475
+ "stale-pin.evidence.json",
1476
+ "unsafe-upgrade.evidence.json",
1477
+ "chain-pass.evidence.json",
1478
+ "chain-fail.evidence.json",
1479
+ ]) {
1480
+ const evidence = readJson(`examples/upgrade-evidence/${file}`);
1481
+ assertSchemaValid(upgradeEvidenceSchema, evidence, file);
1482
+ assert.equal(evidence.changedState.changed, false, file);
1483
+ assert.doesNotMatch(JSON.stringify(evidence), /\/home\/|projectId|github_pat_|ghp_/i);
1484
+ }
1485
+ });
1486
+
1487
+ test("adapter upgrade JSON and explicit output remain schema-valid and bounded", () => {
1488
+ const fixtureRoot = path.join(
1489
+ root,
1490
+ "tests",
1491
+ "fixtures",
1492
+ "project-adapter-upgrades",
1493
+ "valid-upgrade",
1494
+ );
1495
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "upgrade-output-"));
1496
+
1497
+ try {
1498
+ const json = adapterUpgradeCliResult(
1499
+ path.join(fixtureRoot, "before"),
1500
+ path.join(fixtureRoot, "after"),
1501
+ { coreRoot: root, json: true },
1502
+ );
1503
+ assert.equal(json.exitCode, 0);
1504
+ assert.equal(json.stream, "stdout");
1505
+ assertSchemaValid(upgradeEvidenceSchema, JSON.parse(json.lines[0]), "pair JSON");
1506
+
1507
+ const written = adapterUpgradeCliResult(
1508
+ path.join(fixtureRoot, "before"),
1509
+ path.join(fixtureRoot, "after"),
1510
+ {
1511
+ coreRoot: root,
1512
+ output: "upgrade.json",
1513
+ outputBase: temporaryRoot,
1514
+ },
1515
+ );
1516
+ assert.equal(written.exitCode, 0);
1517
+ const output = JSON.parse(
1518
+ fs.readFileSync(path.join(temporaryRoot, "upgrade.json"), "utf8"),
1519
+ );
1520
+ assertSchemaValid(upgradeEvidenceSchema, output, "pair output");
1521
+ assert.equal(output.changedState.changed, false);
1522
+
1523
+ const overwrite = adapterUpgradeCliResult(
1524
+ path.join(fixtureRoot, "before"),
1525
+ path.join(fixtureRoot, "after"),
1526
+ {
1527
+ coreRoot: root,
1528
+ output: "upgrade.json",
1529
+ outputBase: temporaryRoot,
1530
+ },
1531
+ );
1532
+ assert.equal(overwrite.exitCode, 2);
1533
+ assert.match(overwrite.lines.join("\n"), /output-already-exists/);
1534
+
1535
+ for (const unsafe of ["../outside.json", ".env.json", "/tmp/outside.json"]) {
1536
+ const rejected = adapterUpgradeCliResult(
1537
+ path.join(fixtureRoot, "before"),
1538
+ path.join(fixtureRoot, "after"),
1539
+ {
1540
+ coreRoot: root,
1541
+ output: unsafe,
1542
+ outputBase: temporaryRoot,
1543
+ },
1544
+ );
1545
+ assert.equal(rejected.exitCode, 2, unsafe);
1546
+ assert.match(rejected.lines.join("\n"), /unsafe-output-path/, unsafe);
1547
+ }
1548
+
1549
+ const realOutput = path.join(temporaryRoot, "real");
1550
+ fs.mkdirSync(realOutput);
1551
+ fs.symlinkSync(realOutput, path.join(temporaryRoot, "linked"));
1552
+ const symlinked = adapterUpgradeCliResult(
1553
+ path.join(fixtureRoot, "before"),
1554
+ path.join(fixtureRoot, "after"),
1555
+ {
1556
+ coreRoot: root,
1557
+ output: "linked/report.json",
1558
+ outputBase: temporaryRoot,
1559
+ },
1560
+ );
1561
+ assert.equal(symlinked.exitCode, 2);
1562
+ assert.match(symlinked.lines.join("\n"), /output-symlink-escape/);
1563
+ } finally {
1564
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1565
+ }
1566
+ });
1567
+
1568
+ test("adapter upgrade chains accept safe revisions and reject named fixture drift", () => {
1569
+ const fixtureRoot = path.join(
1570
+ root,
1571
+ "tests",
1572
+ "fixtures",
1573
+ "project-adapter-upgrade-chains",
1574
+ );
1575
+ const valid = checkAdapterUpgradeChain(path.join(fixtureRoot, "valid-chain"), {
1576
+ coreRoot: root,
1577
+ });
1578
+ assert.equal(valid.ok, true, valid.codes.join(","));
1579
+ assert.equal(valid.revisionCount, 7);
1580
+ assert.equal(valid.transitionCount, 6);
1581
+
1582
+ for (const [fixture, code] of [
1583
+ ["stale-pin-chain", "stale-exact-pin"],
1584
+ ["broken-compatibility-chain", "skill-compatibility-drift"],
1585
+ ["unsafe-weakening-chain", "restriction-weakening"],
1586
+ ["schema-drift-chain", "adapter-schema-drift"],
1587
+ ["skill-drift-chain", "skill-compatibility-drift"],
1588
+ ]) {
1589
+ const result = checkAdapterUpgradeChain(path.join(fixtureRoot, fixture), {
1590
+ coreRoot: root,
1591
+ });
1592
+ assert.equal(result.ok, false, fixture);
1593
+ assert.ok(result.codes.includes(code), `${fixture}: ${result.codes}`);
1594
+ }
1595
+ });
1596
+
1597
+ test("adapter chain evidence is schema-valid and summarizes ordinal transitions", () => {
1598
+ const fixtureRoot = path.join(
1599
+ root,
1600
+ "tests",
1601
+ "fixtures",
1602
+ "project-adapter-upgrade-chains",
1603
+ );
1604
+ for (const fixture of ["valid-chain", "unsafe-weakening-chain"]) {
1605
+ const result = adapterChainCliResult(path.join(fixtureRoot, fixture), {
1606
+ coreRoot: root,
1607
+ json: true,
1608
+ invocationId: `test-${fixture}`,
1609
+ chainId: `test-${fixture}`,
1610
+ timestamp: "2026-06-14T12:00:00Z",
1611
+ });
1612
+ const evidence = JSON.parse(result.lines[0]);
1613
+ assertSchemaValid(upgradeEvidenceSchema, evidence, fixture);
1614
+ assert.equal(evidence.changedState.changed, false);
1615
+ assert.ok(evidence.chainSummary.steps.length > 0);
1616
+ assert.match(evidence.chainSummary.steps[0].beforeRevision, /^revision-/);
1617
+ assert.doesNotMatch(JSON.stringify(evidence), /01-current|fixture-chain-project/);
1618
+ }
1619
+ });
1620
+
1621
+ test("adapter chains reject dynamic evidence, mode, failure, completion, and version drift", () => {
1622
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "chain-drift-"));
1623
+ const source = path.join(
1624
+ root,
1625
+ "tests",
1626
+ "fixtures",
1627
+ "project-adapter-upgrade-chains",
1628
+ "valid-chain",
1629
+ );
1630
+
1631
+ function prepare(name) {
1632
+ const destination = path.join(temporaryRoot, name);
1633
+ fs.cpSync(source, destination, { recursive: true });
1634
+ return {
1635
+ root: destination,
1636
+ declaration: path.join(destination, "03-upgrade", ".coding-agent", "skills.json"),
1637
+ adapter: path.join(
1638
+ destination,
1639
+ "03-upgrade",
1640
+ ".coding-agent",
1641
+ "adapters",
1642
+ "fixture-chain-adapter",
1643
+ "adapter.json",
1644
+ ),
1645
+ middleDeclaration: path.join(
1646
+ destination,
1647
+ "02-upgrade",
1648
+ ".coding-agent",
1649
+ "skills.json",
1650
+ ),
1651
+ middleAdapter: path.join(
1652
+ destination,
1653
+ "02-upgrade",
1654
+ ".coding-agent",
1655
+ "adapters",
1656
+ "fixture-chain-adapter",
1657
+ "adapter.json",
1658
+ ),
1659
+ };
1660
+ }
1661
+
1662
+ function editJson(file, callback) {
1663
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
1664
+ callback(value);
1665
+ fs.writeFileSync(file, JSON.stringify(value));
1666
+ }
1667
+
1668
+ try {
1669
+ for (const [name, code, mutate] of [
1670
+ [
1671
+ "evidence",
1672
+ "required-evidence-removal",
1673
+ ({ adapter }) =>
1674
+ editJson(adapter, (value) => {
1675
+ value.extensions.requiredEvidence = ["repository root"];
1676
+ }),
1677
+ ],
1678
+ [
1679
+ "failure",
1680
+ "failure-suppression",
1681
+ ({ adapter }) =>
1682
+ editJson(adapter, (value) => {
1683
+ value.inheritance.allowFailureSuppression = true;
1684
+ }),
1685
+ ],
1686
+ [
1687
+ "completion",
1688
+ "completion-override",
1689
+ ({ adapter }) =>
1690
+ editJson(adapter, (value) => {
1691
+ value.inheritance.allowCompletionOverride = true;
1692
+ }),
1693
+ ],
1694
+ [
1695
+ "mode",
1696
+ "mode-escalation",
1697
+ ({ adapter }) =>
1698
+ editJson(adapter, (value) => {
1699
+ value.supportedSkills[0].declaredMode = "action-capable";
1700
+ }),
1701
+ ],
1702
+ [
1703
+ "adapter-version",
1704
+ "adapter-version-drift",
1705
+ ({ declaration, adapter }) => {
1706
+ editJson(declaration, (value) => {
1707
+ value.adapters[0].version = "1.0.1";
1708
+ });
1709
+ editJson(adapter, (value) => {
1710
+ value.adapterVersion = "1.0.1";
1711
+ });
1712
+ },
1713
+ ],
1714
+ [
1715
+ "core-jump",
1716
+ "incompatible-core-chain",
1717
+ ({ middleDeclaration, middleAdapter }) => {
1718
+ editJson(middleDeclaration, (value) => {
1719
+ value.core.expectedVersion = "0.1.6";
1720
+ value.core.versionPin = "0.1.6";
1721
+ });
1722
+ editJson(middleAdapter, (value) => {
1723
+ value.supportedSkills[0].compatibleVersions = ["0.1.6"];
1724
+ });
1725
+ },
1726
+ ],
1727
+ ]) {
1728
+ const chain = prepare(name);
1729
+ mutate(chain);
1730
+ const result = checkAdapterUpgradeChain(chain.root, { coreRoot: root });
1731
+ assert.equal(result.ok, false, name);
1732
+ assert.ok(result.codes.includes(code), `${name}: ${result.codes}`);
1733
+ }
1734
+ } finally {
1735
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1736
+ }
1737
+ });
1738
+
1739
+ test("adapter chain discovery ignores .env, preserves revisions, and redacts secrets", () => {
1740
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "chain-privacy-"));
1741
+ const source = path.join(
1742
+ root,
1743
+ "tests",
1744
+ "fixtures",
1745
+ "project-adapter-upgrade-chains",
1746
+ "valid-chain",
1747
+ );
1748
+ const syntheticValue = readJson("tests/fixtures/privacy/cases.json")
1749
+ .cases.find((candidate) => candidate.id === "fake-github-token")
1750
+ .parts.join("");
1751
+
1752
+ try {
1753
+ const safe = path.join(temporaryRoot, "safe");
1754
+ fs.cpSync(source, safe, { recursive: true });
1755
+ fs.writeFileSync(path.join(safe, ".env"), `SYNTHETIC=${syntheticValue}\n`);
1756
+ fs.writeFileSync(
1757
+ path.join(safe, "02-upgrade", ".env.local"),
1758
+ `SYNTHETIC=${syntheticValue}\n`,
1759
+ );
1760
+ const before = snapshotAbsoluteDirectory(safe);
1761
+ const safeResult = checkAdapterUpgradeChain(safe, { coreRoot: root });
1762
+ const after = snapshotAbsoluteDirectory(safe);
1763
+ assert.equal(safeResult.ok, true, safeResult.codes.join(","));
1764
+ assert.equal(after, before, "chain validation mutated a project revision");
1765
+
1766
+ const secret = path.join(temporaryRoot, "secret");
1767
+ fs.cpSync(source, secret, { recursive: true });
1768
+ const declaration = path.join(
1769
+ secret,
1770
+ "03-upgrade",
1771
+ ".coding-agent",
1772
+ "skills.json",
1773
+ );
1774
+ const value = JSON.parse(fs.readFileSync(declaration, "utf8"));
1775
+ value.syntheticNote = syntheticValue;
1776
+ fs.writeFileSync(declaration, JSON.stringify(value));
1777
+ const secretResult = checkAdapterUpgradeChain(secret, { coreRoot: root });
1778
+ assert.equal(secretResult.ok, false);
1779
+ assert.ok(secretResult.codes.includes("secret-exposure"));
1780
+ assert.doesNotMatch(
1781
+ formatAdapterChainSummary(secretResult).join("\n"),
1782
+ new RegExp(syntheticValue),
1783
+ );
1784
+ const evidence = adapterChainCliResult(secret, {
1785
+ coreRoot: root,
1786
+ json: true,
1787
+ }).lines[0];
1788
+ assert.doesNotMatch(evidence, new RegExp(syntheticValue));
1789
+ } finally {
1790
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1791
+ }
1792
+ });
1793
+
1794
+ test("adapter chain discovery rejects symlink escapes and non-contiguous order", () => {
1795
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "chain-path-"));
1796
+ const source = path.join(
1797
+ root,
1798
+ "tests",
1799
+ "fixtures",
1800
+ "project-adapter-upgrade-chains",
1801
+ "valid-chain",
1802
+ );
1803
+
1804
+ try {
1805
+ const rootLink = path.join(temporaryRoot, "root-link");
1806
+ fs.symlinkSync(source, rootLink);
1807
+ assert.deepEqual(checkAdapterUpgradeChain(rootLink, { coreRoot: root }).codes, [
1808
+ "symlink-escape",
1809
+ ]);
1810
+
1811
+ const linkedRevision = path.join(temporaryRoot, "linked-revision");
1812
+ fs.cpSync(source, linkedRevision, { recursive: true });
1813
+ fs.symlinkSync(
1814
+ path.join(source, "03-upgrade"),
1815
+ path.join(linkedRevision, "04-linked"),
1816
+ );
1817
+ assert.ok(
1818
+ checkAdapterUpgradeChain(linkedRevision, { coreRoot: root }).codes.includes(
1819
+ "symlink-escape",
1820
+ ),
1821
+ );
1822
+
1823
+ const gap = path.join(temporaryRoot, "gap");
1824
+ fs.cpSync(source, gap, { recursive: true });
1825
+ fs.renameSync(path.join(gap, "07-upgrade"), path.join(gap, "08-upgrade"));
1826
+ assert.ok(
1827
+ checkAdapterUpgradeChain(gap, { coreRoot: root }).codes.includes(
1828
+ "non-contiguous-chain-order",
1829
+ ),
1830
+ );
1831
+ } finally {
1832
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1833
+ }
1834
+ });
1835
+
1836
+ test("adapter chain CLI uses stable exits, safe JSON, and bounded output", () => {
1837
+ const fixtureRoot = path.join(
1838
+ root,
1839
+ "tests",
1840
+ "fixtures",
1841
+ "project-adapter-upgrade-chains",
1842
+ );
1843
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "chain-output-"));
1844
+
1845
+ try {
1846
+ const valid = adapterChainCliResult(path.join(fixtureRoot, "valid-chain"), {
1847
+ coreRoot: root,
1848
+ });
1849
+ assert.equal(valid.exitCode, 0);
1850
+ assert.equal(valid.stream, "stdout");
1851
+ assert.match(valid.lines.join("\n"), /6 transitions accepted/);
1852
+
1853
+ const invalid = adapterChainCliResult(
1854
+ path.join(fixtureRoot, "stale-pin-chain"),
1855
+ { coreRoot: root },
1856
+ );
1857
+ assert.equal(invalid.exitCode, 1);
1858
+ assert.equal(invalid.stream, "stderr");
1859
+ assert.match(invalid.lines.join("\n"), /stale-exact-pin/);
1860
+
1861
+ const written = adapterChainCliResult(path.join(fixtureRoot, "valid-chain"), {
1862
+ coreRoot: root,
1863
+ output: "chain.json",
1864
+ outputBase: temporaryRoot,
1865
+ });
1866
+ assert.equal(written.exitCode, 0);
1867
+ assertSchemaValid(
1868
+ upgradeEvidenceSchema,
1869
+ JSON.parse(fs.readFileSync(path.join(temporaryRoot, "chain.json"), "utf8")),
1870
+ "chain output",
1871
+ );
1872
+
1873
+ const traversal = adapterChainCliResult(
1874
+ path.join(fixtureRoot, "valid-chain"),
1875
+ {
1876
+ coreRoot: root,
1877
+ output: "../chain.json",
1878
+ outputBase: temporaryRoot,
1879
+ },
1880
+ );
1881
+ assert.equal(traversal.exitCode, 2);
1882
+ assert.match(traversal.lines.join("\n"), /unsafe-output-path/);
1883
+
1884
+ const usage = adapterChainCliResult(undefined, { coreRoot: root });
1885
+ assert.equal(usage.exitCode, 2);
1886
+ assert.match(usage.lines.join("\n"), /usage:/i);
1887
+ } finally {
1888
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
1889
+ }
1890
+ });
1891
+
1892
+ test("evidence bundles verify hashes, schemas, replay, and regression state", () => {
1893
+ const fixtureRoot = path.join(root, "tests", "fixtures", "evidence-bundles");
1894
+ const bundle = readJson("tests/fixtures/evidence-bundles/valid-bundle/evidence-bundle.json");
1895
+ assertSchemaValid(evidenceBundleSchema, bundle, "valid evidence bundle");
1896
+
1897
+ const first = verifyEvidenceBundle(
1898
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
1899
+ { coreRoot: root },
1900
+ );
1901
+ const second = verifyEvidenceBundle(
1902
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
1903
+ { coreRoot: root },
1904
+ );
1905
+ assert.equal(first.ok, true, first.codes.join(","));
1906
+ assert.equal(first.entryCount, 2);
1907
+ assert.equal(first.replay.deterministic, true);
1908
+ assert.equal(first.replay.reportHash, second.replay.reportHash);
1909
+ assert.deepEqual(first.regression.codes, []);
1910
+ assert.deepEqual(first.retention.codes, []);
1911
+ assert.equal(first.retention.expiryAdvisory.status, "retained");
1912
+ assert.equal(first.retention.expiryAdvisory.deleteAutomatically, false);
1913
+ assert.deepEqual(first.provenance.codes, []);
1914
+ assert.equal(first.provenance.signature.verificationPlan.validatesSignatureNow, false);
1915
+ assert.deepEqual(first.archive.codes, []);
1916
+ assert.equal(first.archive.index.status, "present");
1917
+ assert.deepEqual(first.archive.index.entryIds, ["repo-map-evidence", "upgrade-evidence"]);
1918
+ assert.equal(first.changedState.changed, false);
1919
+ });
1920
+
1921
+ test("evidence bundles report retention-expiry advisories without deleting", () => {
1922
+ const fixtureRoot = path.join(root, "tests", "fixtures", "evidence-bundles");
1923
+ const result = verifyEvidenceBundle(
1924
+ path.join(fixtureRoot, "advisory-review-soon", "evidence-bundle.json"),
1925
+ { coreRoot: root },
1926
+ );
1927
+ assert.equal(result.ok, true, result.codes.join(","));
1928
+ assert.equal(result.retention.expiryAdvisory.status, "review-soon");
1929
+ assert.equal(result.retention.expiryAdvisory.advisoryOnly, true);
1930
+ assert.equal(result.retention.expiryAdvisory.deleteAutomatically, false);
1931
+ });
1932
+
1933
+ test("evidence bundles reject hash, missing-entry, regression, path, and archive failures", () => {
1934
+ const fixtureRoot = path.join(root, "tests", "fixtures", "evidence-bundles");
1935
+ for (const [fixture, code] of [
1936
+ ["invalid-hash", "hash-mismatch"],
1937
+ ["invalid-missing-entry", "entry-missing"],
1938
+ ["invalid-regression", "missing-baseline-entry"],
1939
+ ["invalid-path", "entry-path-traversal"],
1940
+ ["invalid-retention", "retention-retain-until-too-soon"],
1941
+ ["invalid-provenance", "provenance-tag-mismatch"],
1942
+ ["invalid-archive", "archive-raw-evidence-enabled"],
1943
+ ["invalid-archive-index", "archive-index-bundle-mismatch"],
1944
+ ["invalid-signature-plan", "provenance-verification-plan-runs-signature-check"],
1945
+ ]) {
1946
+ const result = verifyEvidenceBundle(
1947
+ path.join(fixtureRoot, fixture, "evidence-bundle.json"),
1948
+ { coreRoot: root },
1949
+ );
1950
+ assert.equal(result.ok, false, fixture);
1951
+ assert.ok(result.codes.includes(code), `${fixture}: ${result.codes}`);
1952
+ }
1953
+ });
1954
+
1955
+ test("evidence bundle CLI uses stable exits and sanitized reports", () => {
1956
+ const fixtureRoot = path.join(root, "tests", "fixtures", "evidence-bundles");
1957
+ const valid = evidenceBundleCliResult(
1958
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
1959
+ { coreRoot: root },
1960
+ );
1961
+ assert.equal(valid.exitCode, 0);
1962
+ assert.equal(valid.stream, "stdout");
1963
+ assert.match(valid.lines.join("\n"), /deterministic replay accepted/);
1964
+
1965
+ const invalid = evidenceBundleCliResult(
1966
+ path.join(fixtureRoot, "invalid-hash", "evidence-bundle.json"),
1967
+ { coreRoot: root },
1968
+ );
1969
+ assert.equal(invalid.exitCode, 1);
1970
+ assert.equal(invalid.stream, "stderr");
1971
+ assert.match(invalid.lines.join("\n"), /hash-mismatch/);
1972
+
1973
+ const json = evidenceBundleCliResult(
1974
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
1975
+ { coreRoot: root, json: true },
1976
+ );
1977
+ assert.equal(json.exitCode, 0);
1978
+ assert.doesNotMatch(json.lines[0], /Repository identity|outputSummary/);
1979
+ assert.doesNotMatch(json.lines[0], /\/home\/|github_pat_|Authorization: Bearer/);
1980
+ });
1981
+
1982
+ test("evidence archive reports are schema-valid, deterministic, and sanitized", () => {
1983
+ const fixtureRoot = path.join(root, "tests", "fixtures", "evidence-bundles");
1984
+ const first = buildEvidenceArchiveReport(
1985
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
1986
+ { coreRoot: root },
1987
+ );
1988
+ const second = buildEvidenceArchiveReport(
1989
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
1990
+ { coreRoot: root },
1991
+ );
1992
+ assert.equal(first.ok, true, first.codes.join(","));
1993
+ assert.equal(first.deterministic, true);
1994
+ assert.equal(first.reportHash, second.reportHash);
1995
+ assertSchemaValid(evidenceArchiveReportSchema, first.report, "archive report");
1996
+ assert.equal(first.report.changedState.changed, false);
1997
+ assert.equal(first.report.archive.writePolicy, "no-write-without-approval");
1998
+ assert.equal(first.report.archive.index.status, "present");
1999
+ assert.equal(first.report.retention.expiryAdvisory.status, "retained");
2000
+ assert.equal(first.report.retention.expiryAdvisory.deleteAutomatically, false);
2001
+ assert.equal(
2002
+ first.report.provenance.signature.verificationPlan.mode,
2003
+ "detached-signature-verification-plan",
2004
+ );
2005
+ assert.equal(first.report.provenance.signature.verificationPlan.validatesSignatureNow, false);
2006
+ assertSchemaValid(
2007
+ evidenceArchiveIndexSchema,
2008
+ readJson("tests/fixtures/evidence-bundles/valid-bundle/archive/evidence-archive-index.json"),
2009
+ "archive index",
2010
+ );
2011
+ const encoded = JSON.stringify(first.report);
2012
+ assert.doesNotMatch(
2013
+ encoded,
2014
+ /commandExecutionRecords|rawEvidence|github_pat_|Authorization: Bearer|\/home\//,
2015
+ );
2016
+ });
2017
+
2018
+ test("evidence archive CLI uses stable exits and bounded sanitized summaries", () => {
2019
+ const fixtureRoot = path.join(root, "tests", "fixtures", "evidence-bundles");
2020
+ const valid = evidenceArchiveCliResult(
2021
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
2022
+ { coreRoot: root },
2023
+ );
2024
+ assert.equal(valid.exitCode, 0);
2025
+ assert.equal(valid.stream, "stdout");
2026
+ assert.match(valid.lines.join("\n"), /sanitized summary accepted/);
2027
+
2028
+ const json = evidenceArchiveCliResult(
2029
+ path.join(fixtureRoot, "valid-bundle", "evidence-bundle.json"),
2030
+ { coreRoot: root, json: true },
2031
+ );
2032
+ assert.equal(json.exitCode, 0);
2033
+ assertSchemaValid(evidenceArchiveReportSchema, JSON.parse(json.lines[0]), "archive CLI JSON");
2034
+
2035
+ const invalid = evidenceArchiveCliResult(
2036
+ path.join(fixtureRoot, "invalid-archive", "evidence-bundle.json"),
2037
+ { coreRoot: root },
2038
+ );
2039
+ assert.equal(invalid.exitCode, 1);
2040
+ assert.equal(invalid.stream, "stderr");
2041
+ assert.match(invalid.lines.join("\n"), /archive-raw-evidence-enabled/);
2042
+ assert.doesNotMatch(
2043
+ invalid.lines.join("\n"),
2044
+ /repo-map\.evidence|valid-upgrade\.evidence|\/home\//,
2045
+ );
2046
+
2047
+ const usage = evidenceArchiveCliResult(undefined, { coreRoot: root });
2048
+ assert.equal(usage.exitCode, 2);
2049
+ assert.match(usage.lines.join("\n"), /usage:/i);
2050
+ });
2051
+
2052
+ test("audit-only agent prompts preserve their non-mutation boundary", () => {
2053
+ for (const skill of AUDIT_ONLY_SKILLS) {
2054
+ const metadata = read(`skills/${skill}/agents/openai.yaml`);
2055
+ assert.match(metadata, /default_prompt:/);
2056
+ assert.match(metadata, /without (?:modifying|changing|rewriting)/i);
2057
+ }
2058
+ });
2059
+
2060
+ test("internal Markdown links resolve", () => {
2061
+ for (const file of walk(root).filter((candidate) => candidate.endsWith(".md"))) {
2062
+ const text = fs.readFileSync(file, "utf8");
2063
+ for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
2064
+ const link = match[1];
2065
+ if (
2066
+ link.startsWith("#") ||
2067
+ /^[a-z]+:/i.test(link) ||
2068
+ link.includes("<") ||
2069
+ link.includes(">")
2070
+ ) {
2071
+ continue;
2072
+ }
2073
+ const target = path.resolve(path.dirname(file), link.split("#")[0]);
2074
+ assert.ok(fs.existsSync(target), `${path.relative(root, file)}: ${link}`);
2075
+ }
2076
+ }
2077
+ });
2078
+
2079
+ test("tracked candidate files contain no obvious secret values", () => {
2080
+ const patterns = [
2081
+ /\bgh[pousr]_[A-Za-z0-9_]{12,}\b/,
2082
+ new RegExp(`\\b${"github" + "_pat_"}[A-Za-z0-9_]{12,}\\b`),
2083
+ /\beyJ[A-Za-z0-9._-]{20,}\b/,
2084
+ new RegExp(["-----BEGIN ", "(?:RSA |EC |OPENSSH )?", "PRIVATE KEY-----"].join("")),
2085
+ /Authorization:\s*Bearer\s+[A-Za-z0-9._-]{8,}/i,
2086
+ ];
2087
+
2088
+ for (const file of walk(root).filter((candidate) =>
2089
+ /\.(?:md|json|yaml|yml|mjs|js)$/.test(candidate),
2090
+ )) {
2091
+ const text = fs.readFileSync(file, "utf8");
2092
+ for (const pattern of patterns) {
2093
+ assert.equal(pattern.test(text), false, path.relative(root, file));
2094
+ }
2095
+ }
2096
+ });
2097
+
2098
+ test("privacy fixtures detect and redact synthetic sensitive shapes", () => {
2099
+ const fixture = readJson("tests/fixtures/privacy/cases.json");
2100
+ assert.equal(fixture.synthetic, true);
2101
+ assert.equal(fixture.encoding, "ordered-parts");
2102
+
2103
+ for (const candidate of fixture.cases) {
2104
+ const syntheticValue = candidate.parts.join("");
2105
+ const detected = detectSensitiveValues(syntheticValue);
2106
+ for (const expected of candidate.expectedTypes) {
2107
+ assert.ok(detected.includes(expected), `${candidate.id}: missing ${expected}`);
2108
+ }
2109
+ const redacted = redactSensitiveText(syntheticValue);
2110
+ assert.deepEqual(detectSensitiveValues(redacted), [], candidate.id);
2111
+ assert.ok(redacted.includes("[REDACTED:"), candidate.id);
2112
+ }
2113
+ });
2114
+
2115
+ test("reusable skill content contains no sensitive-looking values", () => {
2116
+ const reusableFiles = walk(path.join(root, "skills"))
2117
+ .concat(walk(path.join(root, "examples")))
2118
+ .filter((file) => /\.(?:md|json|yaml|yml)$/.test(file));
2119
+
2120
+ for (const file of reusableFiles) {
2121
+ assert.deepEqual(
2122
+ detectSensitiveValues(fs.readFileSync(file, "utf8")),
2123
+ [],
2124
+ path.relative(root, file),
2125
+ );
2126
+ }
2127
+ });
2128
+
2129
+ test("safe executable examples do not contain restricted shell operations", () => {
2130
+ const files = [
2131
+ "CONTRIBUTING.md",
2132
+ ...PILOT_SKILLS.map((skill) => `examples/workflows/${skill}.md`),
2133
+ ];
2134
+
2135
+ for (const file of files) {
2136
+ for (const block of fencedShellBlocks(read(file))) {
2137
+ for (const line of block.split(/\r?\n/)) {
2138
+ const reason = restrictedShellReason(line);
2139
+ assert.equal(reason, null, `${file}: ${reason}: ${line}`);
2140
+ }
2141
+ }
2142
+ }
2143
+ });
2144
+
2145
+ test("mutation fixtures distinguish procedures from explicit denials", () => {
2146
+ const fixture = readJson("tests/fixtures/mutation/cases.json");
2147
+ for (const candidate of fixture.cases) {
2148
+ const issues = auditOnlyDocumentIssues(candidate.document);
2149
+ assert.equal(issues.length, candidate.issues, candidate.id);
2150
+ }
2151
+ });
2152
+
2153
+ test("audit-only skill documents remain non-mutating and snapshot state is unchanged", () => {
2154
+ const snapshotPath = "tests/fixtures/mutation/snapshot-target";
2155
+ const before = snapshotDirectory(snapshotPath);
2156
+
2157
+ for (const skill of AUDIT_ONLY_SKILLS) {
2158
+ const skillDirectory = path.join(root, "skills", skill);
2159
+ for (const file of walk(skillDirectory).filter((candidate) => candidate.endsWith(".md"))) {
2160
+ assert.deepEqual(
2161
+ auditOnlyDocumentIssues(fs.readFileSync(file, "utf8")),
2162
+ [],
2163
+ path.relative(root, file),
2164
+ );
2165
+ }
2166
+ }
2167
+
2168
+ assert.equal(snapshotDirectory(snapshotPath), before);
2169
+ });
2170
+
2171
+ test("restricted inline commands are absent from safe skill example sections", () => {
2172
+ const files = PILOT_SKILLS.flatMap((skill) => [
2173
+ `skills/${skill}/examples.md`,
2174
+ `examples/workflows/${skill}.md`,
2175
+ ]);
2176
+
2177
+ for (const file of files) {
2178
+ let unsafeSection = false;
2179
+ for (const line of read(file).split(/\r?\n/)) {
2180
+ if (/^#{1,6}\s+/.test(line)) unsafeSection = /\b(?:unsafe|denied)\b/i.test(line);
2181
+ if (/^\*\*Unsafe(?: and denied)?:\*\*/i.test(line)) unsafeSection = true;
2182
+ for (const match of line.matchAll(/`([^`\n]+)`/g)) {
2183
+ if (!commandLooksExecutable(match[1])) continue;
2184
+ const reason = restrictedShellReason(match[1]);
2185
+ assert.ok(!reason || unsafeSection, `${file}: ${reason}: ${match[1]}`);
2186
+ }
2187
+ }
2188
+ }
2189
+ });
2190
+
2191
+ test("the sample repository remains dependency-free and runnable with built-in Node", async () => {
2192
+ const packageJson = readJson("tests/fixtures/sample-repo/package.json");
2193
+ assert.equal(packageJson.private, true);
2194
+ assert.equal(packageJson.dependencies, undefined);
2195
+ assert.equal(packageJson.devDependencies, undefined);
2196
+
2197
+ const module = await import(
2198
+ `${path.join(root, "tests/fixtures/sample-repo/src/index.js")}?fixture=${Date.now()}`
2199
+ );
2200
+ assert.equal(module.greeting("pilot"), "Hello, pilot.");
2201
+ });
2202
+
2203
+ test(".gitignore protects local environments and generated validation output", () => {
2204
+ const patterns = new Set(read(".gitignore").split(/\r?\n/));
2205
+ for (const pattern of [
2206
+ ".env",
2207
+ ".env.*",
2208
+ "!.env.example",
2209
+ "*.log",
2210
+ "tmp/",
2211
+ ".vscode/",
2212
+ "validation-output/",
2213
+ "test-results/",
2214
+ ]) {
2215
+ assert.ok(patterns.has(pattern), `missing .gitignore rule ${pattern}`);
2216
+ }
2217
+ });
2218
+
2219
+ let passed = 0;
2220
+ for (const { name, callback } of tests) {
2221
+ try {
2222
+ await callback();
2223
+ passed += 1;
2224
+ console.log(`ok ${passed} - ${name}`);
2225
+ } catch (error) {
2226
+ console.error(`not ok ${passed + 1} - ${name}`);
2227
+ console.error(error.stack ?? error.message);
2228
+ process.exit(1);
2229
+ }
2230
+ }
2231
+
2232
+ console.log(`release tests passed: ${passed}`);