@ahmed-g-gad/apothem 0.1.1

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 (674) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/LICENSES/MIT.txt +18 -0
  4. package/LICENSES/PSF-2.0.txt +47 -0
  5. package/README.md +549 -0
  6. package/bin/README.md +37 -0
  7. package/bin/apothem.mjs +78 -0
  8. package/package.json +75 -0
  9. package/pyproject.toml +347 -0
  10. package/src/apothem/README.md +52 -0
  11. package/src/apothem/__init__.py +66 -0
  12. package/src/apothem/__main__.py +28 -0
  13. package/src/apothem/_vendor/.keep +0 -0
  14. package/src/apothem/_vendor/__init__.py +25 -0
  15. package/src/apothem/_vendor/attr/__init__.py +104 -0
  16. package/src/apothem/_vendor/attr/__init__.pyi +389 -0
  17. package/src/apothem/_vendor/attr/_cmp.py +160 -0
  18. package/src/apothem/_vendor/attr/_cmp.pyi +13 -0
  19. package/src/apothem/_vendor/attr/_compat.py +99 -0
  20. package/src/apothem/_vendor/attr/_config.py +31 -0
  21. package/src/apothem/_vendor/attr/_funcs.py +497 -0
  22. package/src/apothem/_vendor/attr/_make.py +3406 -0
  23. package/src/apothem/_vendor/attr/_next_gen.py +674 -0
  24. package/src/apothem/_vendor/attr/_typing_compat.pyi +15 -0
  25. package/src/apothem/_vendor/attr/_version_info.py +89 -0
  26. package/src/apothem/_vendor/attr/_version_info.pyi +9 -0
  27. package/src/apothem/_vendor/attr/converters.py +162 -0
  28. package/src/apothem/_vendor/attr/converters.pyi +19 -0
  29. package/src/apothem/_vendor/attr/exceptions.py +95 -0
  30. package/src/apothem/_vendor/attr/exceptions.pyi +17 -0
  31. package/src/apothem/_vendor/attr/filters.py +72 -0
  32. package/src/apothem/_vendor/attr/filters.pyi +6 -0
  33. package/src/apothem/_vendor/attr/py.typed +0 -0
  34. package/src/apothem/_vendor/attr/setters.py +79 -0
  35. package/src/apothem/_vendor/attr/setters.pyi +20 -0
  36. package/src/apothem/_vendor/attr/validators.py +750 -0
  37. package/src/apothem/_vendor/attr/validators.pyi +140 -0
  38. package/src/apothem/_vendor/attr.LICENSE +21 -0
  39. package/src/apothem/_vendor/attrs/__init__.py +72 -0
  40. package/src/apothem/_vendor/attrs/__init__.pyi +314 -0
  41. package/src/apothem/_vendor/attrs/converters.py +3 -0
  42. package/src/apothem/_vendor/attrs/exceptions.py +3 -0
  43. package/src/apothem/_vendor/attrs/filters.py +3 -0
  44. package/src/apothem/_vendor/attrs/py.typed +0 -0
  45. package/src/apothem/_vendor/attrs/setters.py +3 -0
  46. package/src/apothem/_vendor/attrs/validators.py +3 -0
  47. package/src/apothem/_vendor/attrs.LICENSE +21 -0
  48. package/src/apothem/_vendor/jsonschema/__init__.py +120 -0
  49. package/src/apothem/_vendor/jsonschema/__main__.py +6 -0
  50. package/src/apothem/_vendor/jsonschema/_format.py +546 -0
  51. package/src/apothem/_vendor/jsonschema/_keywords.py +449 -0
  52. package/src/apothem/_vendor/jsonschema/_legacy_keywords.py +449 -0
  53. package/src/apothem/_vendor/jsonschema/_types.py +204 -0
  54. package/src/apothem/_vendor/jsonschema/_typing.py +29 -0
  55. package/src/apothem/_vendor/jsonschema/_utils.py +355 -0
  56. package/src/apothem/_vendor/jsonschema/benchmarks/__init__.py +5 -0
  57. package/src/apothem/_vendor/jsonschema/benchmarks/const_vs_enum.py +30 -0
  58. package/src/apothem/_vendor/jsonschema/benchmarks/contains.py +28 -0
  59. package/src/apothem/_vendor/jsonschema/benchmarks/import_benchmark.py +31 -0
  60. package/src/apothem/_vendor/jsonschema/benchmarks/issue232/issue.json +2653 -0
  61. package/src/apothem/_vendor/jsonschema/benchmarks/issue232.py +25 -0
  62. package/src/apothem/_vendor/jsonschema/benchmarks/json_schema_test_suite.py +12 -0
  63. package/src/apothem/_vendor/jsonschema/benchmarks/nested_schemas.py +56 -0
  64. package/src/apothem/_vendor/jsonschema/benchmarks/subcomponents.py +42 -0
  65. package/src/apothem/_vendor/jsonschema/benchmarks/unused_registry.py +35 -0
  66. package/src/apothem/_vendor/jsonschema/benchmarks/useless_applicator_schemas.py +106 -0
  67. package/src/apothem/_vendor/jsonschema/benchmarks/useless_keywords.py +32 -0
  68. package/src/apothem/_vendor/jsonschema/benchmarks/validator_creation.py +14 -0
  69. package/src/apothem/_vendor/jsonschema/cli.py +292 -0
  70. package/src/apothem/_vendor/jsonschema/exceptions.py +490 -0
  71. package/src/apothem/_vendor/jsonschema/protocols.py +230 -0
  72. package/src/apothem/_vendor/jsonschema/validators.py +1410 -0
  73. package/src/apothem/_vendor/jsonschema.LICENSE +19 -0
  74. package/src/apothem/_vendor/jsonschema_specifications/__init__.py +12 -0
  75. package/src/apothem/_vendor/jsonschema_specifications/_core.py +38 -0
  76. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/metaschema.json +42 -0
  77. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/applicator +56 -0
  78. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/content +17 -0
  79. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/core +57 -0
  80. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/format +14 -0
  81. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data +37 -0
  82. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/validation +98 -0
  83. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/metaschema.json +58 -0
  84. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/applicator +48 -0
  85. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/content +17 -0
  86. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/core +51 -0
  87. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation +14 -0
  88. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion +14 -0
  89. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data +37 -0
  90. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated +15 -0
  91. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/validation +98 -0
  92. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft3/metaschema.json +172 -0
  93. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft4/metaschema.json +149 -0
  94. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft6/metaschema.json +153 -0
  95. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft7/metaschema.json +166 -0
  96. package/src/apothem/_vendor/jsonschema_specifications.LICENSE +19 -0
  97. package/src/apothem/_vendor/referencing/__init__.py +7 -0
  98. package/src/apothem/_vendor/referencing/_attrs.py +31 -0
  99. package/src/apothem/_vendor/referencing/_attrs.pyi +21 -0
  100. package/src/apothem/_vendor/referencing/_core.py +739 -0
  101. package/src/apothem/_vendor/referencing/exceptions.py +165 -0
  102. package/src/apothem/_vendor/referencing/jsonschema.py +642 -0
  103. package/src/apothem/_vendor/referencing/py.typed +0 -0
  104. package/src/apothem/_vendor/referencing/retrieval.py +94 -0
  105. package/src/apothem/_vendor/referencing/typing.py +61 -0
  106. package/src/apothem/_vendor/referencing.LICENSE +19 -0
  107. package/src/apothem/_vendor/rpds/__init__.py +251 -0
  108. package/src/apothem/_vendor/typing_extensions.LICENSE +279 -0
  109. package/src/apothem/_vendor/typing_extensions.py +4317 -0
  110. package/src/apothem/_vendor/vendor.txt +22 -0
  111. package/src/apothem/_vendor/yaml/__init__.py +389 -0
  112. package/src/apothem/_vendor/yaml/composer.py +138 -0
  113. package/src/apothem/_vendor/yaml/constructor.py +748 -0
  114. package/src/apothem/_vendor/yaml/cyaml.py +100 -0
  115. package/src/apothem/_vendor/yaml/dumper.py +61 -0
  116. package/src/apothem/_vendor/yaml/emitter.py +1137 -0
  117. package/src/apothem/_vendor/yaml/error.py +74 -0
  118. package/src/apothem/_vendor/yaml/events.py +85 -0
  119. package/src/apothem/_vendor/yaml/loader.py +63 -0
  120. package/src/apothem/_vendor/yaml/nodes.py +48 -0
  121. package/src/apothem/_vendor/yaml/parser.py +588 -0
  122. package/src/apothem/_vendor/yaml/reader.py +185 -0
  123. package/src/apothem/_vendor/yaml/representer.py +388 -0
  124. package/src/apothem/_vendor/yaml/resolver.py +226 -0
  125. package/src/apothem/_vendor/yaml/scanner.py +1435 -0
  126. package/src/apothem/_vendor/yaml/serializer.py +110 -0
  127. package/src/apothem/_vendor/yaml/tokens.py +103 -0
  128. package/src/apothem/_vendor/yaml.LICENSE +20 -0
  129. package/src/apothem/agents/README.md +60 -0
  130. package/src/apothem/agents/codebase-explorer.md +91 -0
  131. package/src/apothem/agents/convention-auditor.md +93 -0
  132. package/src/apothem/agents/dependency-auditor.md +97 -0
  133. package/src/apothem/agents/fact-checker.md +84 -0
  134. package/src/apothem/agents/mcp-builder.md +86 -0
  135. package/src/apothem/agents/memory-auditor.md +93 -0
  136. package/src/apothem/agents/prompt-evaluator.md +87 -0
  137. package/src/apothem/agents/quality-gate.md +103 -0
  138. package/src/apothem/agents/refactor-surgeon.md +74 -0
  139. package/src/apothem/agents/research-scout.md +73 -0
  140. package/src/apothem/agents/security-scanner.md +83 -0
  141. package/src/apothem/agents/test-runner.md +84 -0
  142. package/src/apothem/audit/README.md +73 -0
  143. package/src/apothem/audit/_scan_lib.py +182 -0
  144. package/src/apothem/audit/analyze_graph.py +260 -0
  145. package/src/apothem/audit/build_capability_graph.py +607 -0
  146. package/src/apothem/audit/build_inventory.py +657 -0
  147. package/src/apothem/audit/build_plans_provenance.py +997 -0
  148. package/src/apothem/audit/check_links.py +389 -0
  149. package/src/apothem/audit/classify_artifacts.py +381 -0
  150. package/src/apothem/audit/deprecated-tokens.txt +10 -0
  151. package/src/apothem/audit/execute_plans_migration.py +491 -0
  152. package/src/apothem/audit/known-projects.txt +15 -0
  153. package/src/apothem/audit/render_capability_index.py +467 -0
  154. package/src/apothem/audit/render_inventory.py +405 -0
  155. package/src/apothem/audit/scan_ai_surfaces.py +1125 -0
  156. package/src/apothem/audit/scan_ai_surfaces_coarse.py +261 -0
  157. package/src/apothem/audit/scan_drift_features.py +143 -0
  158. package/src/apothem/audit/scan_frontmatter.py +293 -0
  159. package/src/apothem/audit/scan_header_coverage.py +1134 -0
  160. package/src/apothem/audit/scan_plan_leakage.py +540 -0
  161. package/src/apothem/audit/scan_plans_discipline.py +188 -0
  162. package/src/apothem/audit/scan_secrets_pii.py +245 -0
  163. package/src/apothem/audit/scan_stale_tokens.py +296 -0
  164. package/src/apothem/audit/synthesize_drift.py +205 -0
  165. package/src/apothem/benchmarks/README.md +33 -0
  166. package/src/apothem/benchmarks/__init__.py +3 -0
  167. package/src/apothem/benchmarks/bench_agents.py +63 -0
  168. package/src/apothem/benchmarks/bench_hooks.py +93 -0
  169. package/src/apothem/benchmarks/bench_install.py +58 -0
  170. package/src/apothem/benchmarks/bench_tests.py +93 -0
  171. package/src/apothem/benchmarks/bench_validate_ecosystem.py +84 -0
  172. package/src/apothem/cli/README.md +33 -0
  173. package/src/apothem/cli/__init__.py +229 -0
  174. package/src/apothem/cli/_cmd_completion.py +88 -0
  175. package/src/apothem/cli/_cmd_diff.py +181 -0
  176. package/src/apothem/cli/_cmd_doctor.py +143 -0
  177. package/src/apothem/cli/_cmd_harnesses.py +167 -0
  178. package/src/apothem/cli/_cmd_install.py +327 -0
  179. package/src/apothem/cli/_cmd_migrate_workspace.py +143 -0
  180. package/src/apothem/cli/_cmd_profile.py +341 -0
  181. package/src/apothem/cli/_cmd_status.py +180 -0
  182. package/src/apothem/cli/_cmd_uninstall.py +215 -0
  183. package/src/apothem/cli/_cmd_update.py +397 -0
  184. package/src/apothem/cli/_cmd_verify.py +194 -0
  185. package/src/apothem/cli/_common_flags.py +90 -0
  186. package/src/apothem/cli/_epilogs.py +296 -0
  187. package/src/apothem/cli/_helpers.py +857 -0
  188. package/src/apothem/cli/_json_formatter.py +21 -0
  189. package/src/apothem/cli/_materialize.py +376 -0
  190. package/src/apothem/cli/completions/apothem.bash +30 -0
  191. package/src/apothem/cli/completions/apothem.fish +19 -0
  192. package/src/apothem/cli/completions/apothem.ps1 +27 -0
  193. package/src/apothem/cli/completions/apothem.zsh +42 -0
  194. package/src/apothem/cli/reference_export.py +126 -0
  195. package/src/apothem/commands/README.md +125 -0
  196. package/src/apothem/commands/a11y-audit.md +203 -0
  197. package/src/apothem/commands/architecture-review.md +194 -0
  198. package/src/apothem/commands/audit.md +165 -0
  199. package/src/apothem/commands/code-audit.md +218 -0
  200. package/src/apothem/commands/code-review.md +193 -0
  201. package/src/apothem/commands/dependency-audit.md +209 -0
  202. package/src/apothem/commands/docs-review.md +199 -0
  203. package/src/apothem/commands/elevate.md +285 -0
  204. package/src/apothem/commands/eval.md +149 -0
  205. package/src/apothem/commands/fortress.md +172 -0
  206. package/src/apothem/commands/freshify.md +168 -0
  207. package/src/apothem/commands/github-deploy-fresh.md +178 -0
  208. package/src/apothem/commands/github-deploy-next.md +167 -0
  209. package/src/apothem/commands/perf-audit.md +198 -0
  210. package/src/apothem/commands/plan-amend.md +104 -0
  211. package/src/apothem/commands/plan-audit.md +127 -0
  212. package/src/apothem/commands/plan-design.md +257 -0
  213. package/src/apothem/commands/plan-execute.md +495 -0
  214. package/src/apothem/commands/plan-generate.md +351 -0
  215. package/src/apothem/commands/plan-review.md +555 -0
  216. package/src/apothem/commands/plan-spec.md +359 -0
  217. package/src/apothem/commands/plan-status.md +222 -0
  218. package/src/apothem/commands/plan.md +173 -0
  219. package/src/apothem/commands/projectify.md +142 -0
  220. package/src/apothem/commands/release-readiness.md +142 -0
  221. package/src/apothem/commands/research-analysis.md +241 -0
  222. package/src/apothem/commands/research-design.md +231 -0
  223. package/src/apothem/commands/research-disseminate.md +225 -0
  224. package/src/apothem/commands/research-experiment.md +232 -0
  225. package/src/apothem/commands/research-ideate.md +213 -0
  226. package/src/apothem/commands/research-paper.md +252 -0
  227. package/src/apothem/commands/research-proposal.md +220 -0
  228. package/src/apothem/commands/research-publish.md +255 -0
  229. package/src/apothem/commands/research-review.md +251 -0
  230. package/src/apothem/commands/research-sources.md +266 -0
  231. package/src/apothem/commands/research-spec.md +255 -0
  232. package/src/apothem/commands/research-synthesis.md +233 -0
  233. package/src/apothem/commands/research-theory.md +218 -0
  234. package/src/apothem/commands/research.md +181 -0
  235. package/src/apothem/commands/security-audit.md +196 -0
  236. package/src/apothem/commands/supply-chain-audit.md +192 -0
  237. package/src/apothem/commands/test-suite.md +146 -0
  238. package/src/apothem/commands/threat-model-audit.md +199 -0
  239. package/src/apothem/commands/ux-review.md +202 -0
  240. package/src/apothem/commands/workflow.md +162 -0
  241. package/src/apothem/conformity/README.md +173 -0
  242. package/src/apothem/conformity/__init__.py +1 -0
  243. package/src/apothem/conformity/_grep_base.py +93 -0
  244. package/src/apothem/conformity/agent_capability_grep.py +306 -0
  245. package/src/apothem/conformity/agents_md_coverage_grep.py +382 -0
  246. package/src/apothem/conformity/agnosticism_grep.py +311 -0
  247. package/src/apothem/conformity/always_on_budget_grep.py +318 -0
  248. package/src/apothem/conformity/bare_except_grep.py +115 -0
  249. package/src/apothem/conformity/binding_reciprocity_grep.py +151 -0
  250. package/src/apothem/conformity/brand_mark_grep.py +272 -0
  251. package/src/apothem/conformity/commented_out_code_grep.py +176 -0
  252. package/src/apothem/conformity/completion_claim_grep.py +169 -0
  253. package/src/apothem/conformity/conventional_commit_grep.py +319 -0
  254. package/src/apothem/conformity/copilot_instructions_presence_grep.py +324 -0
  255. package/src/apothem/conformity/cross_platform_matrix_grep.py +297 -0
  256. package/src/apothem/conformity/determinism_grep.py +306 -0
  257. package/src/apothem/conformity/diagram_staleness_grep.py +154 -0
  258. package/src/apothem/conformity/dynamism_grep.py +284 -0
  259. package/src/apothem/conformity/editorconfig_presence_grep.py +281 -0
  260. package/src/apothem/conformity/file_header_grep.py +502 -0
  261. package/src/apothem/conformity/freshness_token_grep.py +233 -0
  262. package/src/apothem/conformity/frontmatter_grep.py +274 -0
  263. package/src/apothem/conformity/frontmatter_value_grep.py +386 -0
  264. package/src/apothem/conformity/gate.py +1386 -0
  265. package/src/apothem/conformity/gitattributes_presence_grep.py +238 -0
  266. package/src/apothem/conformity/harden_runner_grep.py +320 -0
  267. package/src/apothem/conformity/hedging_grep.py +129 -0
  268. package/src/apothem/conformity/license_author_consistency_grep.py +204 -0
  269. package/src/apothem/conformity/link_check.py +327 -0
  270. package/src/apothem/conformity/magic_number_grep.py +182 -0
  271. package/src/apothem/conformity/multi_surface_coherence_grep.py +620 -0
  272. package/src/apothem/conformity/naming_grep.py +224 -0
  273. package/src/apothem/conformity/no_global_plans_grep.py +339 -0
  274. package/src/apothem/conformity/no_toplevel_docs_grep.py +120 -0
  275. package/src/apothem/conformity/oidc_trusted_publishing_grep.py +291 -0
  276. package/src/apothem/conformity/option_annotation_grep.py +352 -0
  277. package/src/apothem/conformity/orphan_output_grep.py +206 -0
  278. package/src/apothem/conformity/permissions_minimum_scope_grep.py +299 -0
  279. package/src/apothem/conformity/plain_language_grep.py +559 -0
  280. package/src/apothem/conformity/plan_next_step_consistency_grep.py +450 -0
  281. package/src/apothem/conformity/plan_suite_structure_grep.py +534 -0
  282. package/src/apothem/conformity/plans_discipline_language_grep.py +245 -0
  283. package/src/apothem/conformity/production_ready_pr_grep.py +200 -0
  284. package/src/apothem/conformity/recommend_next_step_grep.py +250 -0
  285. package/src/apothem/conformity/redundancy_grep.py +401 -0
  286. package/src/apothem/conformity/reference_token_grep.py +230 -0
  287. package/src/apothem/conformity/registry_capability_consistency_grep.py +368 -0
  288. package/src/apothem/conformity/secret_leak_grep.py +193 -0
  289. package/src/apothem/conformity/semver_stability_grep.py +358 -0
  290. package/src/apothem/conformity/smoke_install_grep.py +194 -0
  291. package/src/apothem/conformity/static_version_grep.py +284 -0
  292. package/src/apothem/conformity/token_efficiency_grep.py +185 -0
  293. package/src/apothem/conformity/unpinned_action_grep.py +115 -0
  294. package/src/apothem/conformity/user_confirm_grep.py +74 -0
  295. package/src/apothem/conformity/workflow_concurrency_grep.py +283 -0
  296. package/src/apothem/harnesses/README.md +63 -0
  297. package/src/apothem/harnesses/__init__.py +16 -0
  298. package/src/apothem/harnesses/_shared/README.md +36 -0
  299. package/src/apothem/harnesses/_shared/__init__.py +12 -0
  300. package/src/apothem/harnesses/_shared/install_driver.py +281 -0
  301. package/src/apothem/harnesses/_shared/install_driver_apply.py +612 -0
  302. package/src/apothem/harnesses/_shared/install_driver_backup.py +535 -0
  303. package/src/apothem/harnesses/_shared/install_driver_converters.py +310 -0
  304. package/src/apothem/harnesses/_shared/install_driver_lifecycle.py +495 -0
  305. package/src/apothem/harnesses/_shared/install_driver_materialize.py +675 -0
  306. package/src/apothem/harnesses/_shared/install_driver_merge.py +656 -0
  307. package/src/apothem/harnesses/_shared/install_driver_pathsafety.py +137 -0
  308. package/src/apothem/harnesses/_shared/install_driver_planvalidation.py +240 -0
  309. package/src/apothem/harnesses/_shared/install_driver_removal.py +366 -0
  310. package/src/apothem/harnesses/_shared/install_driver_treeops.py +248 -0
  311. package/src/apothem/harnesses/_shared/install_driver_types.py +330 -0
  312. package/src/apothem/harnesses/_shared/wrapper_factories.py +448 -0
  313. package/src/apothem/harnesses/antigravity/STANDARD-CONVENTION-PIN.md +91 -0
  314. package/src/apothem/harnesses/antigravity/__init__.py +70 -0
  315. package/src/apothem/harnesses/antigravity/capabilities.yml +40 -0
  316. package/src/apothem/harnesses/antigravity/install.py +63 -0
  317. package/src/apothem/harnesses/antigravity/templates/GEMINI.md +40 -0
  318. package/src/apothem/harnesses/antigravity/templates/plugin.json +5 -0
  319. package/src/apothem/harnesses/antigravity/uninstall.py +22 -0
  320. package/src/apothem/harnesses/antigravity/update.py +10 -0
  321. package/src/apothem/harnesses/antigravity/verify.py +11 -0
  322. package/src/apothem/harnesses/claude_code/STANDARD-CONVENTION-PIN.md +65 -0
  323. package/src/apothem/harnesses/claude_code/__init__.py +107 -0
  324. package/src/apothem/harnesses/claude_code/capabilities.yml +42 -0
  325. package/src/apothem/harnesses/claude_code/install.py +147 -0
  326. package/src/apothem/harnesses/claude_code/templates/settings.json +351 -0
  327. package/src/apothem/harnesses/claude_code/uninstall.py +23 -0
  328. package/src/apothem/harnesses/claude_code/update.py +10 -0
  329. package/src/apothem/harnesses/claude_code/verify.py +11 -0
  330. package/src/apothem/harnesses/codebuddy/STANDARD-CONVENTION-PIN.md +74 -0
  331. package/src/apothem/harnesses/codebuddy/__init__.py +49 -0
  332. package/src/apothem/harnesses/codebuddy/capabilities.yml +34 -0
  333. package/src/apothem/harnesses/codebuddy/install.py +40 -0
  334. package/src/apothem/harnesses/codebuddy/templates/apothem-rules.md +37 -0
  335. package/src/apothem/harnesses/codebuddy/uninstall.py +25 -0
  336. package/src/apothem/harnesses/codebuddy/update.py +10 -0
  337. package/src/apothem/harnesses/codebuddy/verify.py +11 -0
  338. package/src/apothem/harnesses/codex/STANDARD-CONVENTION-PIN.md +79 -0
  339. package/src/apothem/harnesses/codex/__init__.py +72 -0
  340. package/src/apothem/harnesses/codex/capabilities.yml +40 -0
  341. package/src/apothem/harnesses/codex/install.py +69 -0
  342. package/src/apothem/harnesses/codex/templates/AGENTS.md +40 -0
  343. package/src/apothem/harnesses/codex/templates/hooks.json +127 -0
  344. package/src/apothem/harnesses/codex/uninstall.py +23 -0
  345. package/src/apothem/harnesses/codex/update.py +10 -0
  346. package/src/apothem/harnesses/codex/verify.py +11 -0
  347. package/src/apothem/harnesses/cursor/STANDARD-CONVENTION-PIN.md +79 -0
  348. package/src/apothem/harnesses/cursor/__init__.py +48 -0
  349. package/src/apothem/harnesses/cursor/capabilities.yml +42 -0
  350. package/src/apothem/harnesses/cursor/install.py +38 -0
  351. package/src/apothem/harnesses/cursor/templates/apothem-rules.mdc +40 -0
  352. package/src/apothem/harnesses/cursor/uninstall.py +25 -0
  353. package/src/apothem/harnesses/cursor/update.py +10 -0
  354. package/src/apothem/harnesses/cursor/verify.py +11 -0
  355. package/src/apothem/harnesses/gemini_cli/STANDARD-CONVENTION-PIN.md +102 -0
  356. package/src/apothem/harnesses/gemini_cli/__init__.py +52 -0
  357. package/src/apothem/harnesses/gemini_cli/capabilities.yml +43 -0
  358. package/src/apothem/harnesses/gemini_cli/install.py +43 -0
  359. package/src/apothem/harnesses/gemini_cli/templates/GEMINI.md +38 -0
  360. package/src/apothem/harnesses/gemini_cli/uninstall.py +25 -0
  361. package/src/apothem/harnesses/gemini_cli/update.py +10 -0
  362. package/src/apothem/harnesses/gemini_cli/verify.py +11 -0
  363. package/src/apothem/harnesses/github_copilot/STANDARD-CONVENTION-PIN.md +84 -0
  364. package/src/apothem/harnesses/github_copilot/__init__.py +47 -0
  365. package/src/apothem/harnesses/github_copilot/capabilities.yml +42 -0
  366. package/src/apothem/harnesses/github_copilot/install.py +40 -0
  367. package/src/apothem/harnesses/github_copilot/templates/copilot-instructions.md +33 -0
  368. package/src/apothem/harnesses/github_copilot/uninstall.py +25 -0
  369. package/src/apothem/harnesses/github_copilot/update.py +10 -0
  370. package/src/apothem/harnesses/github_copilot/verify.py +11 -0
  371. package/src/apothem/harnesses/glm/STANDARD-CONVENTION-PIN.md +77 -0
  372. package/src/apothem/harnesses/glm/__init__.py +56 -0
  373. package/src/apothem/harnesses/glm/capabilities.yml +33 -0
  374. package/src/apothem/harnesses/glm/install.py +45 -0
  375. package/src/apothem/harnesses/glm/templates/glm.toml +58 -0
  376. package/src/apothem/harnesses/glm/uninstall.py +25 -0
  377. package/src/apothem/harnesses/glm/update.py +10 -0
  378. package/src/apothem/harnesses/glm/verify.py +11 -0
  379. package/src/apothem/harnesses/hermes/STANDARD-CONVENTION-PIN.md +57 -0
  380. package/src/apothem/harnesses/hermes/__init__.py +33 -0
  381. package/src/apothem/harnesses/hermes/capabilities.yml +36 -0
  382. package/src/apothem/harnesses/hermes/install.py +17 -0
  383. package/src/apothem/harnesses/hermes/materializer.py +35 -0
  384. package/src/apothem/harnesses/hermes/uninstall.py +33 -0
  385. package/src/apothem/harnesses/hermes/update.py +10 -0
  386. package/src/apothem/harnesses/hermes/verify.py +11 -0
  387. package/src/apothem/harnesses/kimi_code/STANDARD-CONVENTION-PIN.md +128 -0
  388. package/src/apothem/harnesses/kimi_code/__init__.py +59 -0
  389. package/src/apothem/harnesses/kimi_code/capabilities.yml +40 -0
  390. package/src/apothem/harnesses/kimi_code/install.py +42 -0
  391. package/src/apothem/harnesses/kimi_code/templates/AGENTS.md +43 -0
  392. package/src/apothem/harnesses/kimi_code/uninstall.py +27 -0
  393. package/src/apothem/harnesses/kimi_code/update.py +10 -0
  394. package/src/apothem/harnesses/kimi_code/verify.py +11 -0
  395. package/src/apothem/harnesses/kiro/STANDARD-CONVENTION-PIN.md +77 -0
  396. package/src/apothem/harnesses/kiro/__init__.py +49 -0
  397. package/src/apothem/harnesses/kiro/capabilities.yml +36 -0
  398. package/src/apothem/harnesses/kiro/install.py +39 -0
  399. package/src/apothem/harnesses/kiro/templates/apothem-rules.md +36 -0
  400. package/src/apothem/harnesses/kiro/uninstall.py +25 -0
  401. package/src/apothem/harnesses/kiro/update.py +10 -0
  402. package/src/apothem/harnesses/kiro/verify.py +11 -0
  403. package/src/apothem/harnesses/open_claw/STANDARD-CONVENTION-PIN.md +62 -0
  404. package/src/apothem/harnesses/open_claw/__init__.py +35 -0
  405. package/src/apothem/harnesses/open_claw/capabilities.yml +35 -0
  406. package/src/apothem/harnesses/open_claw/install.py +17 -0
  407. package/src/apothem/harnesses/open_claw/materializer.py +36 -0
  408. package/src/apothem/harnesses/open_claw/uninstall.py +32 -0
  409. package/src/apothem/harnesses/open_claw/update.py +10 -0
  410. package/src/apothem/harnesses/open_claw/verify.py +11 -0
  411. package/src/apothem/harnesses/opencode/STANDARD-CONVENTION-PIN.md +76 -0
  412. package/src/apothem/harnesses/opencode/__init__.py +35 -0
  413. package/src/apothem/harnesses/opencode/capabilities.yml +43 -0
  414. package/src/apothem/harnesses/opencode/install.py +17 -0
  415. package/src/apothem/harnesses/opencode/materializer.py +31 -0
  416. package/src/apothem/harnesses/opencode/uninstall.py +34 -0
  417. package/src/apothem/harnesses/opencode/update.py +10 -0
  418. package/src/apothem/harnesses/opencode/verify.py +11 -0
  419. package/src/apothem/harnesses/qwen_code/STANDARD-CONVENTION-PIN.md +87 -0
  420. package/src/apothem/harnesses/qwen_code/__init__.py +37 -0
  421. package/src/apothem/harnesses/qwen_code/capabilities.yml +43 -0
  422. package/src/apothem/harnesses/qwen_code/install.py +19 -0
  423. package/src/apothem/harnesses/qwen_code/materializer.py +174 -0
  424. package/src/apothem/harnesses/qwen_code/templates/QWEN.md +30 -0
  425. package/src/apothem/harnesses/qwen_code/uninstall.py +34 -0
  426. package/src/apothem/harnesses/qwen_code/update.py +10 -0
  427. package/src/apothem/harnesses/qwen_code/verify.py +11 -0
  428. package/src/apothem/harnesses/trae/STANDARD-CONVENTION-PIN.md +70 -0
  429. package/src/apothem/harnesses/trae/__init__.py +49 -0
  430. package/src/apothem/harnesses/trae/capabilities.yml +34 -0
  431. package/src/apothem/harnesses/trae/install.py +38 -0
  432. package/src/apothem/harnesses/trae/templates/apothem-rules.md +37 -0
  433. package/src/apothem/harnesses/trae/uninstall.py +25 -0
  434. package/src/apothem/harnesses/trae/update.py +10 -0
  435. package/src/apothem/harnesses/trae/verify.py +11 -0
  436. package/src/apothem/harnesses/windsurf/STANDARD-CONVENTION-PIN.md +91 -0
  437. package/src/apothem/harnesses/windsurf/__init__.py +52 -0
  438. package/src/apothem/harnesses/windsurf/capabilities.yml +40 -0
  439. package/src/apothem/harnesses/windsurf/install.py +41 -0
  440. package/src/apothem/harnesses/windsurf/templates/apothem-rules.md +37 -0
  441. package/src/apothem/harnesses/windsurf/uninstall.py +25 -0
  442. package/src/apothem/harnesses/windsurf/update.py +10 -0
  443. package/src/apothem/harnesses/windsurf/verify.py +11 -0
  444. package/src/apothem/harnesses/zed/STANDARD-CONVENTION-PIN.md +92 -0
  445. package/src/apothem/harnesses/zed/__init__.py +57 -0
  446. package/src/apothem/harnesses/zed/capabilities.yml +38 -0
  447. package/src/apothem/harnesses/zed/install.py +41 -0
  448. package/src/apothem/harnesses/zed/templates/apothem-rules.md +32 -0
  449. package/src/apothem/harnesses/zed/uninstall.py +28 -0
  450. package/src/apothem/harnesses/zed/update.py +10 -0
  451. package/src/apothem/harnesses/zed/verify.py +11 -0
  452. package/src/apothem/hooks/README.md +81 -0
  453. package/src/apothem/hooks/__init__.py +24 -0
  454. package/src/apothem/hooks/askuserquestion_validator.py +380 -0
  455. package/src/apothem/hooks/dispatch.py +296 -0
  456. package/src/apothem/hooks/emit_hook_context.py +444 -0
  457. package/src/apothem/hooks/hooks.json +318 -0
  458. package/src/apothem/hooks/lib/README.md +39 -0
  459. package/src/apothem/hooks/lib/__init__.py +18 -0
  460. package/src/apothem/hooks/lib/bootstrap.ps1 +129 -0
  461. package/src/apothem/hooks/lib/bootstrap.sh +103 -0
  462. package/src/apothem/hooks/lib/events.py +51 -0
  463. package/src/apothem/hooks/lib/find-pwsh.ps1 +78 -0
  464. package/src/apothem/hooks/lib/find-pwsh.sh +76 -0
  465. package/src/apothem/hooks/lib/find-python.ps1 +63 -0
  466. package/src/apothem/hooks/lib/find-python.sh +97 -0
  467. package/src/apothem/hooks/lib/log.py +43 -0
  468. package/src/apothem/hooks/lib/resolve_root.py +264 -0
  469. package/src/apothem/hooks/messages/postcompact.md +14 -0
  470. package/src/apothem/hooks/messages/posttooluse-proactive-compaction.md +46 -0
  471. package/src/apothem/hooks/messages/precompact.md +14 -0
  472. package/src/apothem/hooks/messages/pretooluse-askuserquestion-recommended.md +65 -0
  473. package/src/apothem/hooks/messages/pretooluse-bash-plan-guard.md +97 -0
  474. package/src/apothem/hooks/messages/pretooluse-bash.md +39 -0
  475. package/src/apothem/hooks/messages/pretooluse-conformity.md +70 -0
  476. package/src/apothem/hooks/messages/pretooluse-dependency-guard.md +21 -0
  477. package/src/apothem/hooks/messages/pretooluse-edit-header-guard.md +61 -0
  478. package/src/apothem/hooks/messages/pretooluse-edit.md +21 -0
  479. package/src/apothem/hooks/messages/pretooluse-eval-guard.md +39 -0
  480. package/src/apothem/hooks/messages/pretooluse-notebookedit.md +11 -0
  481. package/src/apothem/hooks/messages/pretooluse-write-header-guard.md +45 -0
  482. package/src/apothem/hooks/messages/pretooluse-write-plan-guard.md +72 -0
  483. package/src/apothem/hooks/messages/pretooluse-write.md +21 -0
  484. package/src/apothem/hooks/messages/sessionstart.md +15 -0
  485. package/src/apothem/hooks/messages/stop.md +27 -0
  486. package/src/apothem/hooks/proactive_compaction_tracker.py +327 -0
  487. package/src/apothem/hooks/session_start_bootstrap.py +472 -0
  488. package/src/apothem/lib/README.md +42 -0
  489. package/src/apothem/lib/__init__.py +13 -0
  490. package/src/apothem/lib/atomic_io.py +189 -0
  491. package/src/apothem/lib/auditor.py +687 -0
  492. package/src/apothem/lib/clean_slate.py +396 -0
  493. package/src/apothem/lib/contexts.py +352 -0
  494. package/src/apothem/lib/data_home.py +255 -0
  495. package/src/apothem/lib/frontmatter.py +101 -0
  496. package/src/apothem/lib/harness_materializer.py +213 -0
  497. package/src/apothem/lib/harness_protocol.py +59 -0
  498. package/src/apothem/lib/harness_registry.py +282 -0
  499. package/src/apothem/lib/harness_registry_data.py +843 -0
  500. package/src/apothem/lib/install_ledger.py +347 -0
  501. package/src/apothem/lib/learning.py +540 -0
  502. package/src/apothem/lib/memory.py +347 -0
  503. package/src/apothem/lib/parallel_sweep.py +234 -0
  504. package/src/apothem/lib/plan_tiers.py +200 -0
  505. package/src/apothem/lib/plugin_bootstrap.py +132 -0
  506. package/src/apothem/lib/plugin_tree.py +599 -0
  507. package/src/apothem/lib/profile.py +755 -0
  508. package/src/apothem/lib/profile_projection.py +198 -0
  509. package/src/apothem/lib/propagation-manifest.yaml +878 -0
  510. package/src/apothem/lib/propagation.py +220 -0
  511. package/src/apothem/lib/python_resolver.py +189 -0
  512. package/src/apothem/lib/reporter.py +62 -0
  513. package/src/apothem/lib/workspace_migration.py +323 -0
  514. package/src/apothem/output-styles/README.md +41 -0
  515. package/src/apothem/output-styles/concise-engineer.md +49 -0
  516. package/src/apothem/output-styles/default-architect.md +52 -0
  517. package/src/apothem/output-styles/default.md +113 -0
  518. package/src/apothem/output-styles/forensic-auditor.md +63 -0
  519. package/src/apothem/py.typed +0 -0
  520. package/src/apothem/rules/README.md +121 -0
  521. package/src/apothem/rules/agent-capability-discipline-matrix.md +89 -0
  522. package/src/apothem/rules/agent-capability-discipline.md +78 -0
  523. package/src/apothem/rules/agent-orchestration-patterns.md +144 -0
  524. package/src/apothem/rules/agent-orchestration.md +65 -0
  525. package/src/apothem/rules/agents-md-convention.md +86 -0
  526. package/src/apothem/rules/agile-sprints-elements.md +135 -0
  527. package/src/apothem/rules/agile-sprints.md +64 -0
  528. package/src/apothem/rules/agnostic-posture-checklist.md +47 -0
  529. package/src/apothem/rules/agnostic-posture.md +48 -0
  530. package/src/apothem/rules/authoritative-referencing-quotation.md +50 -0
  531. package/src/apothem/rules/authoritative-referencing.md +66 -0
  532. package/src/apothem/rules/authority-inquiry-categories.md +58 -0
  533. package/src/apothem/rules/authority-inquiry.md +54 -0
  534. package/src/apothem/rules/auto-memory-topic-files.md +86 -0
  535. package/src/apothem/rules/auto-memory.md +67 -0
  536. package/src/apothem/rules/bidirectional-binding.md +123 -0
  537. package/src/apothem/rules/canonical-layout-reporting-tiers.md +212 -0
  538. package/src/apothem/rules/canonical-layout.md +60 -0
  539. package/src/apothem/rules/clean-architecture-layers.md +186 -0
  540. package/src/apothem/rules/clean-room-generation-protocols.md +124 -0
  541. package/src/apothem/rules/clean-room-generation.md +59 -0
  542. package/src/apothem/rules/code-craft-conventions.md +101 -0
  543. package/src/apothem/rules/code-craft-markdown.md +138 -0
  544. package/src/apothem/rules/code-craft-python.md +154 -0
  545. package/src/apothem/rules/code-craft-shell.md +192 -0
  546. package/src/apothem/rules/cognitive-identity-techniques.md +180 -0
  547. package/src/apothem/rules/cognitive-identity.md +81 -0
  548. package/src/apothem/rules/context-management-budget.md +46 -0
  549. package/src/apothem/rules/context-management-protocol.md +161 -0
  550. package/src/apothem/rules/context-management-scratch.md +128 -0
  551. package/src/apothem/rules/context-management.md +85 -0
  552. package/src/apothem/rules/definitiveness-virtues.md +67 -0
  553. package/src/apothem/rules/definitiveness.md +58 -0
  554. package/src/apothem/rules/determinism.md +81 -0
  555. package/src/apothem/rules/disclosure-ledger-markers.md +58 -0
  556. package/src/apothem/rules/disclosure-ledger.md +52 -0
  557. package/src/apothem/rules/dynamism.md +38 -0
  558. package/src/apothem/rules/etc-extension.md +57 -0
  559. package/src/apothem/rules/expertise-posture-elements.md +68 -0
  560. package/src/apothem/rules/expertise-posture.md +54 -0
  561. package/src/apothem/rules/freshness-facade.md +64 -0
  562. package/src/apothem/rules/harness-adapter-shape-schemas.md +162 -0
  563. package/src/apothem/rules/harness-adapter-shape.md +42 -0
  564. package/src/apothem/rules/host-discovery-manifests.md +50 -0
  565. package/src/apothem/rules/host-discovery.md +56 -0
  566. package/src/apothem/rules/i18n-discipline-locale-cohorts.md +120 -0
  567. package/src/apothem/rules/i18n-discipline.md +70 -0
  568. package/src/apothem/rules/interactive-questions-canonical-shapes.md +590 -0
  569. package/src/apothem/rules/interactive-questions-detail.md +41 -0
  570. package/src/apothem/rules/interactive-questions-sweep-matchers.md +184 -0
  571. package/src/apothem/rules/interactive-questions.md +89 -0
  572. package/src/apothem/rules/large-file-generation.md +112 -0
  573. package/src/apothem/rules/large-file-reading.md +59 -0
  574. package/src/apothem/rules/living-docs.md +85 -0
  575. package/src/apothem/rules/multi-agent-workflow.md +57 -0
  576. package/src/apothem/rules/operational-mandates-expanded.md +78 -0
  577. package/src/apothem/rules/operational-mandates.md +88 -0
  578. package/src/apothem/rules/option-annotation-form.md +60 -0
  579. package/src/apothem/rules/option-annotation.md +45 -0
  580. package/src/apothem/rules/own-voice-reimplementation.md +86 -0
  581. package/src/apothem/rules/performance-discipline.md +91 -0
  582. package/src/apothem/rules/persistent-conventions-vigilance-checklist.md +54 -0
  583. package/src/apothem/rules/persistent-conventions-vigilance.md +61 -0
  584. package/src/apothem/rules/plain-language.md +56 -0
  585. package/src/apothem/rules/planning-techniques.md +130 -0
  586. package/src/apothem/rules/pre-emission-gate-bars.md +86 -0
  587. package/src/apothem/rules/pre-emission-gate.md +54 -0
  588. package/src/apothem/rules/production-ready-prs-surfaces.md +162 -0
  589. package/src/apothem/rules/production-ready-prs.md +83 -0
  590. package/src/apothem/rules/propagation.md +63 -0
  591. package/src/apothem/rules/recommend-next-step.md +106 -0
  592. package/src/apothem/rules/refactoring-discipline.md +76 -0
  593. package/src/apothem/rules/session-closure.md +44 -0
  594. package/src/apothem/rules/sota-elevation-exemplars.md +76 -0
  595. package/src/apothem/rules/sota-elevation.md +52 -0
  596. package/src/apothem/rules/source-accessibility.md +58 -0
  597. package/src/apothem/rules/surgical-manipulation.md +48 -0
  598. package/src/apothem/rules/systemic-participation-relations.md +108 -0
  599. package/src/apothem/rules/systemic-participation.md +70 -0
  600. package/src/apothem/rules/ten-dimension-check-dimensions.md +52 -0
  601. package/src/apothem/rules/ten-dimension-check.md +59 -0
  602. package/src/apothem/rules/token-budget-discipline.md +81 -0
  603. package/src/apothem/rules/token-efficiency-rewrite-protocol.md +79 -0
  604. package/src/apothem/rules/token-efficiency-rewrite.md +77 -0
  605. package/src/apothem/rules/tool-use-discipline.md +48 -0
  606. package/src/apothem/rules/visual-leverage.md +102 -0
  607. package/src/apothem/schemas/NOTICE.md +9 -0
  608. package/src/apothem/schemas/README.md +104 -0
  609. package/src/apothem/schemas/__init__.py +176 -0
  610. package/src/apothem/schemas/advisory-finding.schema.json +111 -0
  611. package/src/apothem/schemas/agent.schema.json +106 -0
  612. package/src/apothem/schemas/authorship-header.txt +1 -0
  613. package/src/apothem/schemas/cohort-manifest.yaml +248 -0
  614. package/src/apothem/schemas/cohort-metadata-vocabulary.yaml +168 -0
  615. package/src/apothem/schemas/cohort.schema.json +113 -0
  616. package/src/apothem/schemas/command.schema.json +68 -0
  617. package/src/apothem/schemas/compatibility-matrix.yaml +432 -0
  618. package/src/apothem/schemas/context-fragment.schema.json +64 -0
  619. package/src/apothem/schemas/freshness-token-denylist.txt +51 -0
  620. package/src/apothem/schemas/handoff-manifest.yaml +353 -0
  621. package/src/apothem/schemas/header-exceptions.txt +141 -0
  622. package/src/apothem/schemas/header-visibility.yaml +39 -0
  623. package/src/apothem/schemas/learning-signal.schema.json +46 -0
  624. package/src/apothem/schemas/memory-record.schema.json +61 -0
  625. package/src/apothem/schemas/output-style.schema.json +40 -0
  626. package/src/apothem/schemas/plan.schema.json +51 -0
  627. package/src/apothem/schemas/plugin.schema.json +83 -0
  628. package/src/apothem/schemas/profile.example.yaml +70 -0
  629. package/src/apothem/schemas/profile.minimal.yaml +6 -0
  630. package/src/apothem/schemas/profile.schema.json +396 -0
  631. package/src/apothem/schemas/reference-token-denylist.txt +25 -0
  632. package/src/apothem/schemas/skill.schema.json +75 -0
  633. package/src/apothem/skills/README.md +93 -0
  634. package/src/apothem/skills/dependency-upgrade/SKILL.md +105 -0
  635. package/src/apothem/skills/dev-toolkit/SKILL.md +120 -0
  636. package/src/apothem/skills/diagram-authoring/SKILL.md +113 -0
  637. package/src/apothem/skills/document-authoring/SKILL.md +118 -0
  638. package/src/apothem/skills/ecosystem-audit/SKILL.md +108 -0
  639. package/src/apothem/skills/ecosystem-audit/references/audit-fortress.md +85 -0
  640. package/src/apothem/skills/ecosystem-audit/references/procedure.md +162 -0
  641. package/src/apothem/skills/eval-harness/SKILL.md +88 -0
  642. package/src/apothem/skills/incident-runbook/SKILL.md +92 -0
  643. package/src/apothem/skills/multi-source-research/SKILL.md +90 -0
  644. package/src/apothem/skills/plan-suite/SKILL.md +118 -0
  645. package/src/apothem/skills/plan-suite/master_template.md +1324 -0
  646. package/src/apothem/skills/projectify/SKILL.md +117 -0
  647. package/src/apothem/skills/prompt-engineering/SKILL.md +122 -0
  648. package/src/apothem/skills/refactor-extract/SKILL.md +85 -0
  649. package/src/apothem/skills/research-suite/SKILL.md +170 -0
  650. package/src/apothem/skills/research-suite/references/directory-structure.md +47 -0
  651. package/src/apothem/skills/research-suite/references/lifecycle.md +67 -0
  652. package/src/apothem/skills/research-suite/references/principal-investigator-framework.md +37 -0
  653. package/src/apothem/skills/research-suite/references/rigor-mandates.md +30 -0
  654. package/src/apothem/skills/research-suite/research_template.md +476 -0
  655. package/src/apothem/skills/secret-rotation/SKILL.md +87 -0
  656. package/src/apothem/skills/source-synthesis/SKILL.md +92 -0
  657. package/src/apothem/skills/surgical-guard/SKILL.md +118 -0
  658. package/src/apothem/skills/test-authoring/SKILL.md +85 -0
  659. package/src/apothem/skills/vuln-triage/SKILL.md +91 -0
  660. package/src/apothem/skills/workflow/SKILL.md +139 -0
  661. package/src/apothem/statuslines/README.md +26 -0
  662. package/src/apothem/statuslines/__init__.py +20 -0
  663. package/src/apothem/statuslines/conformity.json +5 -0
  664. package/src/apothem/statuslines/render.py +334 -0
  665. package/src/apothem/statuslines/statusline.md +50 -0
  666. package/src/apothem/templates/README.md +43 -0
  667. package/src/apothem/templates/agents-md-template.md +80 -0
  668. package/src/apothem/templates/consideration-log.md +39 -0
  669. package/src/apothem/templates/expertise-gap-log.md +56 -0
  670. package/src/apothem/templates/master-index-template.md +93 -0
  671. package/src/apothem/templates/potency-map.md +53 -0
  672. package/src/apothem/templates/preservation-audit.md +60 -0
  673. package/src/apothem/templates/question-resolution-audit.md +52 -0
  674. package/src/apothem/templates/trace-matrix-template.md +77 -0
@@ -0,0 +1,1386 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ """Conformity-gate orchestrator: dispatches every grep against a Write/Edit input.
4
+
5
+ Why this orchestrator exists. The pre-emission gate's mechanical fraction
6
+ is a corpus of per-class greps; the orchestrator is the single dispatch
7
+ surface the harness invokes per `PreToolUse` Write/Edit hook. The
8
+ orchestrator reads the tool-input JSON from stdin, extracts the content
9
+ and target path, runs every grep's `check()` callable in sequence, and
10
+ aggregates findings into a single structured report. The gate is
11
+ advisory by default: it emits the consolidated report plus a per-finding
12
+ summary (so findings are never silent) and exits zero, letting the write
13
+ proceed. Strict mode is opt-in — with the ``--strict`` flag or a truthy
14
+ ``APOTHEM_CONFORMITY_STRICT`` environment variable, a non-empty findings
15
+ list exits non-zero so the harness or CI blocks the action.
16
+
17
+ Performance budget. The hook's wall-clock ceiling is the 10s PreToolUse
18
+ limit per `rules/performance-discipline.md` §1, less the
19
+ PowerShell bootstrap stub's ~1500ms and the interpreter-locator's
20
+ ~200ms; the cumulative grep budget is ~8300ms. Per-grep budget is the
21
+ ``PER_GREP_BUDGET_SECONDS`` constant (~520ms), applied across the registered
22
+ per-Write greps (``len(GREP_MODULES)``) so the figure tracks the live registry
23
+ rather than a hardcoded count. The orchestrator times every grep and reports any
24
+ that approached the per-grep ceiling so future tuning can target the slow path.
25
+
26
+ Per-file vs per-staged-diff dispatch. Most greps run on the content
27
+ plus path. The production-ready-pr grep operates at change-set
28
+ granularity rather than per-file; in the per-Write dispatch path it
29
+ returns clean. The CLI-mode `--staged` flag invokes the substantive
30
+ staged-diff check.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import importlib.util
36
+ import json
37
+ import os
38
+ import subprocess
39
+ import sys
40
+ import time
41
+ from dataclasses import asdict, dataclass, field
42
+ from pathlib import Path
43
+ from typing import Any, Final, Protocol, cast
44
+
45
+ # Environment variable that overrides the default conformity-gate scopes.
46
+ # When set, the gate's --hook mode runs the matcher chain only on writes
47
+ # whose target path falls under this directory; out-of-scope writes
48
+ # short-circuit to a silent pass-through.
49
+ SCOPE_ENV_VAR: Final[str] = "APOTHEM_CONFORMITY_SCOPE"
50
+
51
+ # Default scopes when APOTHEM_CONFORMITY_SCOPE is unset. Hook-capable
52
+ # user-scope harness roots are matcher-applicable territories; writes
53
+ # outside them are pass-through.
54
+ _DEFAULT_CLAUDE_SCOPE: Final[Path] = Path.home() / ".claude"
55
+
56
+
57
+ def _default_scopes() -> tuple[Path, ...]:
58
+ """Return the default user-scope hook-capable harness roots."""
59
+ codex_home = os.environ.get("CODEX_HOME")
60
+ codex_scope = (
61
+ Path(codex_home).expanduser() if codex_home else Path.home() / ".codex"
62
+ )
63
+ return (_DEFAULT_CLAUDE_SCOPE, codex_scope)
64
+
65
+
66
+ def _resolve_scope() -> Path:
67
+ """Return the first configured conformity-gate scope as an absolute Path.
68
+
69
+ Kept for compatibility with tests and callers that inspect the legacy
70
+ single-scope helper. Hook dispatch uses :func:`_resolve_scopes`.
71
+ """
72
+ return _resolve_scopes()[0]
73
+
74
+
75
+ def _resolve_scopes() -> tuple[Path, ...]:
76
+ """Return the configured conformity-gate scopes as absolute Paths.
77
+
78
+ Resolution order: ``APOTHEM_CONFORMITY_SCOPE`` env var > the default
79
+ user-scope hook-capable harness roots. Returned paths are resolved
80
+ (symlinks followed; relative components collapsed) so in-scope
81
+ comparisons work against absolute canonical forms.
82
+ """
83
+ override = os.environ.get(SCOPE_ENV_VAR)
84
+ if override:
85
+ return (Path(override).expanduser().resolve(),)
86
+ return tuple(scope.expanduser().resolve() for scope in _default_scopes())
87
+
88
+
89
+ def _path_in_scope(target: Path | None, scope: Path) -> bool:
90
+ """Return True when *target* lives under *scope*.
91
+
92
+ Resolves *target* to its absolute canonical form, then walks up its
93
+ parent chain looking for *scope*. When *target* is None (no write
94
+ target supplied — e.g., a CLI mode invocation), returns True so the
95
+ matchers run as today; the short-circuit fires only on hook-mode
96
+ invocations with a resolvable out-of-scope target.
97
+ """
98
+ if target is None:
99
+ return True
100
+ try:
101
+ resolved = target.expanduser().resolve()
102
+ except (OSError, RuntimeError):
103
+ # Fail-closed-but-permissive: if path resolution fails, keep
104
+ # matchers running so the gate does not silently drop writes
105
+ # whose targets are simply unusual but in-scope.
106
+ return True
107
+ try:
108
+ resolved.relative_to(scope)
109
+ except ValueError:
110
+ return False
111
+ return True
112
+
113
+
114
+ def _path_in_any_scope(target: Path | None, scopes: tuple[Path, ...]) -> bool:
115
+ """Return True when *target* lives under any configured scope."""
116
+ return any(_path_in_scope(target, scope) for scope in scopes)
117
+
118
+
119
+ def scope_relative_path(target: Path) -> tuple[Path, Path] | None:
120
+ """Return ``(scope_root, path-relative-to-scope)`` for *target*'s scope.
121
+
122
+ Tries each configured conformity scope (``APOTHEM_CONFORMITY_SCOPE`` or the
123
+ default hook-capable harness roots) in resolution order and returns the
124
+ first that contains *target*, with *target* rendered relative to it. Returns
125
+ ``None`` when *target* is under no configured scope. This is the shared seam
126
+ the per-Write matchers use to evaluate a hook-scoped write (e.g.
127
+ ``~/.claude/rules/x.md``) against the same notion of "in scope" the gate
128
+ uses — so a matcher and the gate agree, and a write outside the apothem repo
129
+ root is still checked relative to the harness root it landed under.
130
+ """
131
+ try:
132
+ resolved = target.expanduser().resolve()
133
+ except (OSError, RuntimeError):
134
+ return None
135
+ for scope in _resolve_scopes():
136
+ try:
137
+ return scope, resolved.relative_to(scope)
138
+ except ValueError:
139
+ continue
140
+ return None
141
+
142
+
143
+ # Harness runtime-state subtrees. Under each hook-capable harness root, these
144
+ # top-level subtrees hold the harness's OWN operator-facing state, NOT
145
+ # apothem-managed config:
146
+ # - ``projects/<hash>/`` --- per-project state: project-scoped auto-memory
147
+ # (``memory/MEMORY.md`` plus topic files), session transcripts, todo lists.
148
+ # - ``memory/`` --- the global (cross-project) auto-memory tier.
149
+ # Both are owned by the operator and the harness; apothem never materializes or
150
+ # manages them (apothem's managed config lives in sibling subtrees: ``rules/``,
151
+ # ``skills/``, ``agents/``, ``commands/``, ``hooks/``, ``output-styles/``,
152
+ # ``statuslines/``, ``settings.json``). The hook exempts these subtrees so
153
+ # per-Write dispatch does not block the operator's memory writes --- a
154
+ # ``MEMORY.md`` index is provenance-less and frontmatter-less by the auto-memory
155
+ # convention and would otherwise fail-close on ``orphan-output-grep`` /
156
+ # ``frontmatter-grep``.
157
+ _HARNESS_STATE_SEGMENTS: Final[frozenset[str]] = frozenset({"projects", "memory"})
158
+
159
+
160
+ def _is_harness_state_path(target: Path | None, scopes: tuple[Path, ...]) -> bool:
161
+ """Return True iff *target* sits under a scope's harness runtime-state subtree.
162
+
163
+ The check is scope-coupled: it fires only when *target* resolves under a
164
+ configured harness-config root AND the first path component below that root
165
+ is one of ``_HARNESS_STATE_SEGMENTS`` (``projects`` / ``memory``). A
166
+ coincidental ``projects/`` or ``memory/`` directory in an unrelated
167
+ workspace is not exempted because that workspace is not a configured scope.
168
+ Returns False for ``None`` targets and for paths that fail resolution.
169
+ """
170
+ if target is None:
171
+ return False
172
+ try:
173
+ resolved = target.expanduser().resolve()
174
+ except (OSError, RuntimeError):
175
+ return False
176
+ for scope in scopes:
177
+ try:
178
+ relative = resolved.relative_to(scope)
179
+ except ValueError:
180
+ continue
181
+ if relative.parts and relative.parts[0] in _HARNESS_STATE_SEGMENTS:
182
+ return True
183
+ return False
184
+
185
+
186
+ class _CheckCallable(Protocol):
187
+ """Shape of every grep module's `check()` callable."""
188
+
189
+ def __call__(
190
+ self,
191
+ content: str,
192
+ path: Path | None = None,
193
+ ) -> Any: ... # noqa: ANN401 # GrepResult instances are duck-typed across modules.
194
+
195
+
196
+ # Per-grep dispatch order. The order is intentional: the cheap structural
197
+ # scans (placeholder + arrow notation) run first; the more expensive
198
+ # regex sweeps and the entropy heuristic run later. The dispatch order
199
+ # does not affect the final verdict (every grep is run regardless).
200
+ GREP_MODULES: Final[tuple[str, ...]] = (
201
+ "user_confirm_grep",
202
+ "binding_reciprocity_grep",
203
+ "option_annotation_grep",
204
+ "completion_claim_grep",
205
+ "hedging_grep",
206
+ "brand_mark_grep",
207
+ "diagram_staleness_grep",
208
+ "unpinned_action_grep",
209
+ "bare_except_grep",
210
+ "magic_number_grep",
211
+ "orphan_output_grep",
212
+ "commented_out_code_grep",
213
+ "secret_leak_grep",
214
+ "production_ready_pr_grep",
215
+ "file_header_grep",
216
+ "copilot_instructions_presence_grep",
217
+ "multi_surface_coherence_grep",
218
+ "license_author_consistency_grep",
219
+ "frontmatter_grep",
220
+ "link_check",
221
+ "always_on_budget_grep",
222
+ "token_efficiency_grep",
223
+ # `semver_stability_grep` is change-set-scoped (operates on the git diff
224
+ # between staged + HEAD), not per-Write, so it is NOT in the per-Write
225
+ # registry. Invoke at change-set boundary via
226
+ # `python -m apothem.conformity.semver_stability_grep --staged`.
227
+ )
228
+
229
+ # Corpus-level standalone validators. These do NOT fit the per-Write
230
+ # `check(content, path)` signature — they walk the working tree, the
231
+ # git index, or a fixed surface set. They are invoked via subprocess
232
+ # in --all mode and exposed via --check <name> and --list. Each script
233
+ # accepts a root directory as its sole argument and exits 0 (PASS) or
234
+ # non-zero (FAIL).
235
+ STANDALONE_MODULES: Final[tuple[str, ...]] = (
236
+ "naming-grep",
237
+ "smoke-install-grep",
238
+ "no-global-plans-grep",
239
+ "plan-suite-structure-grep",
240
+ "no-toplevel-docs-grep",
241
+ "plans-discipline-language-grep",
242
+ "plain-language-grep",
243
+ "reference-token-grep",
244
+ "freshness-token-grep",
245
+ "agnosticism-grep",
246
+ "agent-capability-grep",
247
+ "frontmatter-value-grep",
248
+ "static-version-grep",
249
+ "dynamism-grep",
250
+ "oidc-trusted-publishing-grep",
251
+ "recommend-next-step-grep",
252
+ "plan-next-step-consistency-grep",
253
+ "redundancy-grep",
254
+ "conventional-commit-grep",
255
+ "gitattributes-presence-grep",
256
+ "editorconfig-presence-grep",
257
+ "permissions-minimum-scope-grep",
258
+ "cross-platform-matrix-grep",
259
+ "harden-runner-grep",
260
+ "workflow-concurrency-grep",
261
+ "determinism-grep",
262
+ "agents-md-coverage-grep",
263
+ "registry-capability-consistency-grep",
264
+ )
265
+
266
+ # Per-grep wall-clock budget in seconds. Exceeding the budget surfaces
267
+ # as a watch item in the report but does not block the write — the hook
268
+ # has its own 10s ceiling and the orchestrator avoids hard-killing a
269
+ # grep mid-scan.
270
+ PER_GREP_BUDGET_SECONDS: Final[float] = 0.520
271
+
272
+ # Where the grep modules live. The orchestrator resolves them relative
273
+ # to its own location so the hook works regardless of the operator's
274
+ # current working directory and of the layout the package is mounted in
275
+ # (repo checkout ``src/apothem/conformity/`` or installed
276
+ # ``<install-root>/apothem/conformity/`` — the modules and their
277
+ # ``schemas/`` sibling travel together in both shapes).
278
+ TOOLS_DIR: Final[Path] = Path(__file__).resolve().parent
279
+
280
+ EXIT_PASS: Final[int] = 0
281
+ EXIT_FAIL: Final[int] = 2
282
+ STDIN_FLAG: Final[str] = "--stdin"
283
+ CHECK_FLAG: Final[str] = "--check"
284
+ LIST_FLAG: Final[str] = "--list"
285
+ ALL_FLAG: Final[str] = "--all"
286
+ ALL_PERWRITE_FLAG: Final[str] = "--all-perwrite"
287
+ STRICT_FLAG: Final[str] = "--strict"
288
+ STRICT_ENV: Final[str] = "APOTHEM_CONFORMITY_STRICT"
289
+ _STRICT_TRUTHY: Final[frozenset[str]] = frozenset({"1", "true", "yes", "on"})
290
+
291
+ # --- Corpus per-Write runner: blocking vs advisory classification -----------
292
+ #
293
+ # The ``--all-perwrite`` mode routes every git-tracked file through every
294
+ # per-Write matcher in ``GREP_MODULES`` (the corpus counterpart to the
295
+ # harness's per-write hook dispatch). Per the EN-1 ratified posture at
296
+ # ``.plans/apothem-overhaul/ws5-enforcement-posture.md``, each matcher is
297
+ # classified into exactly one of two disjoint sets:
298
+ #
299
+ # - BLOCKING (``_BLOCKING_PER_WRITE_GREPS``): deterministic, low-false-
300
+ # positive matchers that are GREEN over the live tracked corpus today.
301
+ # A finding from one of these under ``--strict`` fails the corpus run.
302
+ # EN-1's governing principle: a matcher is blocking ONLY once it is
303
+ # green over the tracked corpus — never blocking-AND-failing.
304
+ #
305
+ # - ADVISORY (``_ADVISORY_PER_WRITE_GREPS``): matchers whose findings are
306
+ # reported (so the drift is never silent) but do NOT gate, because they
307
+ # are high-false-positive against the shipped prose / code / config /
308
+ # docs corpus today. Each carries a one-line remediation-owner note in
309
+ # ``_ADVISORY_RATIONALE`` so the classification is visible and testable.
310
+ # EN-1 explicitly authorizes ``hedging`` + ``plain-language`` as advisory
311
+ # until the SR-1 (phase 65) prose-debt clears; the remaining advisory
312
+ # members are matchers whose own ``check()`` fires structurally against
313
+ # legitimate non-target content the corpus enumerates (lockfile entropy,
314
+ # frontmatter-first Markdown, design-token literals, documented pattern
315
+ # references), not against a genuine defect a fix could clear here.
316
+ #
317
+ # The two sets PARTITION ``GREP_MODULES`` — every per-Write matcher is in
318
+ # exactly one, verified by ``_assert_perwrite_partition`` at import time so a
319
+ # matcher can never be silently left out of (or in both of) the classification.
320
+
321
+ _BLOCKING_PER_WRITE_GREPS: Final[frozenset[str]] = frozenset(
322
+ {
323
+ "binding_reciprocity_grep",
324
+ "option_annotation_grep",
325
+ "completion_claim_grep",
326
+ "unpinned_action_grep",
327
+ }
328
+ )
329
+
330
+ # Advisory rationale: matcher name -> (reason, remediation-owner). The reason
331
+ # names WHY the matcher is high-false-positive in corpus mode; the owner names
332
+ # the phase / surface that, once it lands, lets a successor promote the matcher
333
+ # to the blocking set. ``SR-1`` is the plain-language / rule-rewrite reconcile
334
+ # phase (phase 65) per the EN-1 posture.
335
+ _ADVISORY_RATIONALE: Final[dict[str, tuple[str, str]]] = {
336
+ "bare_except_grep": (
337
+ "the bare `except:` sub-rule (always a real defect) is GREEN over the "
338
+ "tree; the broad `except Exception:`-without-raise sub-heuristic fires "
339
+ "on deliberate, `# noqa: BLE001`-marked fail-open isolation boundaries "
340
+ "(dispatch / gate / statusline / hook) that ruff's BLE001 rule already "
341
+ "governs and `ruff check` already gates in CI",
342
+ "ruff BLE001 (already CI-gating) + SR-1 reconcile (phase 65)",
343
+ ),
344
+ "user_confirm_grep": (
345
+ "fires on prose / commands / matcher source that DOCUMENT the "
346
+ "<USER-CONFIRM:id=...> placeholder syntax, not on unresolved "
347
+ "placeholders",
348
+ "SR-1 prose reconcile (phase 65)",
349
+ ),
350
+ "hedging_grep": (
351
+ "EN-1-authorized advisory: hedging vocabulary trips a portion of the "
352
+ "shipped rule / doc bodies until the prose is rewritten; SR-1 reconciles "
353
+ "plain-language only, so the rule-body hedging debt is owned by the rule-body "
354
+ "rewrite cluster, not SR-1",
355
+ "SR-4..SR-9 rule-body rewrite cluster",
356
+ ),
357
+ "brand_mark_grep": (
358
+ "fires on harness brand slugs (Cursor, Codex, ...) in config / "
359
+ "workflow / catalog files where the slug is a load-bearing catalog "
360
+ "entry, not a privileging brand reference",
361
+ "SR-1 prose reconcile (phase 65)",
362
+ ),
363
+ "diagram_staleness_grep": (
364
+ "date-comparison heuristic flags shipped docs diagrams whose verified "
365
+ "date predates a sibling edit; staleness reconcile is doc-rewrite work",
366
+ "SR-1 prose reconcile (phase 65)",
367
+ ),
368
+ "magic_number_grep": (
369
+ "fires on CSS design-token values, version pins, and rebuild-script "
370
+ "asset dimensions that are values in a data context, not logic literals",
371
+ "SR-1 prose reconcile (phase 65)",
372
+ ),
373
+ "orphan_output_grep": (
374
+ "orphan-output is a multi-step-work-session concept; in corpus mode it "
375
+ "fires on standalone config / data files that carry no provenance "
376
+ "frontmatter by their own ratified convention",
377
+ "SR-1 prose reconcile (phase 65)",
378
+ ),
379
+ "commented_out_code_grep": (
380
+ "fires on YAML / TOML comment blocks and shell here-doc bodies that "
381
+ "resemble commented-out code but are deliberate inline documentation",
382
+ "SR-1 prose reconcile (phase 65)",
383
+ ),
384
+ "secret_leak_grep": (
385
+ "entropy heuristic fires on package-lock.json integrity hashes and "
386
+ "high-entropy doc tokens (harness slugs, base64-looking examples), and "
387
+ "the literal-shape heuristic fires on test fixtures that DELIBERATELY "
388
+ "embed fake credentials to exercise the detector under test; no genuine "
389
+ "secret among the corpus findings",
390
+ "SR-1 prose reconcile (phase 65)",
391
+ ),
392
+ "production_ready_pr_grep": (
393
+ "change-set-scoped matcher; returns clean per-file by design, so it "
394
+ "carries no corpus verdict and is non-gating in per-file corpus mode",
395
+ "n/a (change-set granularity, not per-file)",
396
+ ),
397
+ "file_header_grep": (
398
+ "the per-Write insertion-at-index-0 logic does not model the "
399
+ "frontmatter-first Markdown class (rules / agents / commands / docs / "
400
+ "AGENTS.md) that is the dominant ratified head convention across the "
401
+ "shipped tree; the genuine code-surface gaps are fixed in source",
402
+ "SR-1 prose reconcile (phase 65)",
403
+ ),
404
+ "copilot_instructions_presence_grep": (
405
+ "single-target surface matcher (.github/copilot-instructions.md); in "
406
+ "corpus mode it returns clean for every non-target file, carrying no "
407
+ "corpus verdict",
408
+ "n/a (single-surface presence check)",
409
+ ),
410
+ "multi_surface_coherence_grep": (
411
+ "cross-surface coherence matcher scoped to the AGENTS.md / CLAUDE.md / "
412
+ "copilot triad; non-target files carry no corpus verdict",
413
+ "n/a (cross-surface coherence check)",
414
+ ),
415
+ "license_author_consistency_grep": (
416
+ "scoped to the LICENSE consistency surface; non-target files carry no "
417
+ "corpus verdict",
418
+ "n/a (single-surface consistency check)",
419
+ ),
420
+ "frontmatter_grep": (
421
+ "class-inference heuristic fires on .mdx / .tsx components and docs content "
422
+ "whose frontmatter contract differs from the rule / skill / agent "
423
+ "schema it infers",
424
+ "SR-1 prose reconcile (phase 65)",
425
+ ),
426
+ "link_check": (
427
+ "link reachability / resolution is inherently high-false-positive over "
428
+ "the corpus (relative-link base ambiguity, external-host flakiness)",
429
+ "SR-1 prose reconcile (phase 65)",
430
+ ),
431
+ "always_on_budget_grep": (
432
+ "surfaces a genuine ~10-token overage on one always-on rule "
433
+ "(interactive-questions.md); remediation is a rule-body decomposition "
434
+ "owned by the token-budget / SR rewrite cluster, not the EN-3 "
435
+ "enforcement-wiring phase",
436
+ "SR token-budget rewrite cluster (SR-0..SR-9, phases 64-73)",
437
+ ),
438
+ "token_efficiency_grep": (
439
+ "filler / qualifier prose heuristic in the same prose-debt class as "
440
+ "hedging; high-false-positive against shipped rule / doc bodies",
441
+ "SR-1 prose reconcile (phase 65)",
442
+ ),
443
+ }
444
+
445
+ _ADVISORY_PER_WRITE_GREPS: Final[frozenset[str]] = frozenset(_ADVISORY_RATIONALE)
446
+
447
+
448
+ def _assert_perwrite_partition() -> None:
449
+ """Verify BLOCKING and ADVISORY partition ``GREP_MODULES`` exactly.
450
+
451
+ Raised at import time so a newly-registered per-Write matcher cannot be
452
+ silently left unclassified (it would fall through corpus mode with no
453
+ blocking / advisory verdict) or double-classified (blocking AND advisory).
454
+ """
455
+ classified = _BLOCKING_PER_WRITE_GREPS | _ADVISORY_PER_WRITE_GREPS
456
+ overlap = _BLOCKING_PER_WRITE_GREPS & _ADVISORY_PER_WRITE_GREPS
457
+ if overlap:
458
+ raise RuntimeError(
459
+ f"per-Write matcher(s) classified BOTH blocking and advisory: "
460
+ f"{sorted(overlap)}"
461
+ )
462
+ registry = set(GREP_MODULES)
463
+ missing = registry - classified
464
+ extra = classified - registry
465
+ if missing:
466
+ raise RuntimeError(
467
+ f"per-Write matcher(s) unclassified for corpus mode: {sorted(missing)}"
468
+ )
469
+ if extra:
470
+ raise RuntimeError(
471
+ f"corpus classification names matcher(s) absent from GREP_MODULES: "
472
+ f"{sorted(extra)}"
473
+ )
474
+
475
+
476
+ _assert_perwrite_partition()
477
+
478
+ # Per-suffix applicability for matchers whose RULE is suffix-scoped but whose
479
+ # own ``check()`` does not gate by file type. Each named matcher inspects only
480
+ # files whose suffix is in its set; a file of any other type is skipped for
481
+ # that matcher (so a Markdown file is never bare-except-scanned, an arbitrary
482
+ # file is never workflow-action-scanned). Matchers absent from this map self-
483
+ # gate inside their own ``check()`` (e.g. magic-number / commented-out-code /
484
+ # file-header by variant family) or are content-type-agnostic prose matchers.
485
+ _PER_SUFFIX_APPLICABILITY: Final[dict[str, frozenset[str]]] = {
486
+ # M13.3 error handling is a Python rule; `except:` is Python syntax.
487
+ "bare_except_grep": frozenset({".py", ".pyi"}),
488
+ # GitHub Actions pinning applies to workflow YAML only.
489
+ "unpinned_action_grep": frozenset({".yml", ".yaml"}),
490
+ }
491
+
492
+ # File suffixes whose bytes are opaque to the text matchers; the corpus runner
493
+ # skips them entirely (it cannot decode them as UTF-8 text). Mirrors the binary
494
+ # extension exemptions at schemas/header-exceptions.txt.
495
+ _CORPUS_BINARY_SUFFIXES: Final[frozenset[str]] = frozenset(
496
+ {
497
+ ".png",
498
+ ".jpg",
499
+ ".jpeg",
500
+ ".gif",
501
+ ".ico",
502
+ ".webp",
503
+ ".svg",
504
+ ".pdf",
505
+ ".zip",
506
+ ".tar",
507
+ ".gz",
508
+ ".tgz",
509
+ ".7z",
510
+ ".rar",
511
+ ".exe",
512
+ ".dll",
513
+ ".so",
514
+ ".dylib",
515
+ ".bin",
516
+ ".pyc",
517
+ ".pyo",
518
+ ".woff",
519
+ ".woff2",
520
+ ".ttf",
521
+ ".otf",
522
+ ".eot",
523
+ ".mp3",
524
+ ".mp4",
525
+ ".webm",
526
+ ".mov",
527
+ ".avi",
528
+ }
529
+ )
530
+
531
+
532
+ @dataclass(frozen=True)
533
+ class GrepInvocation:
534
+ """One grep's result plus its wall-clock timing.
535
+
536
+ ``error`` is non-None only when the matcher's load or ``check()`` raised;
537
+ such an invocation is recorded ``passed=True`` (fail-open isolation) so one
538
+ matcher's internal bug never fail-closes the operator's write, while the
539
+ error text is preserved in the report for diagnosis rather than swallowed.
540
+ """
541
+
542
+ grep: str
543
+ passed: bool
544
+ findings: list[dict[str, object]]
545
+ elapsed_seconds: float
546
+ over_budget: bool
547
+ error: str | None = None
548
+
549
+
550
+ @dataclass(frozen=True)
551
+ class OrchestratorReport:
552
+ """Aggregate result of one conformity-gate run.
553
+
554
+ Carries the overall pass verdict, the scanned path (``None`` in
555
+ corpus mode), the grep / pass / fail counts, and the per-grep
556
+ :class:`GrepInvocation` records.
557
+ """
558
+
559
+ passed: bool
560
+ path: str | None
561
+ grep_count: int
562
+ pass_count: int
563
+ fail_count: int
564
+ invocations: list[GrepInvocation] = field(default_factory=list)
565
+
566
+ def to_json(self) -> str:
567
+ """Serialize the report as indented JSON under the
568
+ ``conformity-gate`` orchestrator envelope."""
569
+ payload = {
570
+ "orchestrator": "conformity-gate",
571
+ "passed": self.passed,
572
+ "path": self.path,
573
+ "grep_count": self.grep_count,
574
+ "pass_count": self.pass_count,
575
+ "fail_count": self.fail_count,
576
+ "invocations": [asdict(i) for i in self.invocations],
577
+ }
578
+ return json.dumps(payload, indent=2)
579
+
580
+
581
+ def _load_check(module_name: str) -> _CheckCallable:
582
+ """Load `module_name.check` via importlib (the file uses hyphens).
583
+
584
+ Why the sys.modules registration matters: Python's dataclasses decorator
585
+ inspects `sys.modules.get(cls.__module__)` at class-creation time to
586
+ resolve forward-referenced KW_ONLY sentinels. Without registering the
587
+ module under its qualified name before `exec_module()`, the lookup
588
+ returns None and the decorator raises AttributeError on Python 3.14+.
589
+ """
590
+ module_path = TOOLS_DIR / f"{module_name}.py"
591
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
592
+ if spec is None or spec.loader is None:
593
+ raise RuntimeError(f"cannot load grep module at {module_path}")
594
+ module = importlib.util.module_from_spec(spec)
595
+ sys.modules[module_name] = module
596
+ try:
597
+ spec.loader.exec_module(module)
598
+ except Exception:
599
+ sys.modules.pop(module_name, None)
600
+ raise
601
+ check = getattr(module, "check", None)
602
+ if not callable(check):
603
+ raise RuntimeError(f"grep module {module_name} exposes no check() callable")
604
+ return check # type: ignore[no-any-return]
605
+
606
+
607
+ def _findings_as_dicts(result: object) -> list[dict[str, object]]:
608
+ """Extract the findings list from a GrepResult-shaped object."""
609
+ findings = getattr(result, "findings", []) or []
610
+ return [
611
+ asdict(f) if hasattr(f, "__dataclass_fields__") else dict(f) for f in findings
612
+ ]
613
+
614
+
615
+ def _is_plan_suite_path(path: Path | None) -> bool:
616
+ """Return True iff path is under the canonical project-local plans subtree.
617
+
618
+ The sole canonical project-local plans location is ``.apothem/plans/`` (the
619
+ shared Apothem working directory's plans child); a legacy ``.plans/`` tree
620
+ is no longer canonical — operators upgrade it via ``apothem
621
+ migrate-workspace``.
622
+
623
+ Plan-suite artifacts are gitignored ephemera per the ``.apothem/plans/**``
624
+ class enumerated at ``schemas/header-exceptions.txt`` (the schemas directory
625
+ shipped beside this package in both the repo-checkout and installed
626
+ layouts) and exempt from the
627
+ codebase quality bar (M13 code-craft, M15 production-ready). The
628
+ orchestrator short-circuits on plan-suite paths so the per-Write hook
629
+ does not block plan-suite emissions whose Markdown enumerations (dates,
630
+ version identifiers, phase identifiers, kebab-case directory paths)
631
+ fire structural false positives on matchers designed for codebase
632
+ content.
633
+
634
+ Per-Write narrow-routing (G1) --- DISCLOSED-DEFERRED. A narrower posture
635
+ would route plans writes through a small per-Write matcher subset
636
+ instead of a blanket synthetic PASS. It is deferred because the only
637
+ per-Write matcher whose rule governs plans-suite structure
638
+ (``orphan_output_grep``) fires "provenance absent" on legitimate
639
+ ``_inputs/`` scratch files (e.g. a ``prose.txt`` or a frontmatter-less
640
+ working note) --- exactly the volatile, session-local scratch tier the
641
+ closed-vocabulary rule declares exempt from the codebase quality bar.
642
+ Routing per-Write writes through it would flood pre-existing findings on
643
+ conformant scratch and invert the advisory posture. The structural
644
+ invariants (suite-locality, closed vocabularies, numeric-prefix
645
+ discipline, phase coherence) are instead enforced at the CORRECT
646
+ granularity by the directory-walking ``plan-suite-structure-grep``
647
+ standalone validator, which runs over the whole tree in ``--all`` repo
648
+ sweeps (CI + pre-commit). A directory-walker is the right shape for
649
+ suite-structure invariants; the per-Write content matcher is not. When a
650
+ per-Write plans matcher whose rule genuinely governs scratch-tier
651
+ content lands, this short-circuit can be narrowed reversibly.
652
+ """
653
+ if path is None:
654
+ return False
655
+ parts = path.parts
656
+ # The sole canonical project-local plans home is ``.apothem/plans`` (the
657
+ # shared Apothem working directory's plans child). Match the two-segment
658
+ # adjacency, never a lone ``.apothem`` part — ``.apothem`` also holds
659
+ # operator memory/learning/contexts data, which is NOT plan-suite content.
660
+ return any(
661
+ parts[i] == ".apothem" and parts[i + 1] == "plans"
662
+ for i in range(len(parts) - 1)
663
+ )
664
+
665
+
666
+ def run_orchestrator(
667
+ content: str,
668
+ path: Path | None,
669
+ only: str | None = None,
670
+ ) -> OrchestratorReport:
671
+ """Run every grep; aggregate the results into a single report.
672
+
673
+ When ``only`` names a single grep module (e.g., ``"file-header-grep"``
674
+ or its short form ``"file-header"``), the orchestrator runs only that
675
+ module. The short form drops the trailing ``-grep`` suffix.
676
+
677
+ Plan-suite paths short-circuit to a synthetic PASS per
678
+ ``_is_plan_suite_path`` --- see that helper's docstring for the rationale.
679
+ The short-circuit applies regardless of ``only`` so per-grep CLI
680
+ invocations on plan-suite paths also pass cleanly.
681
+ """
682
+ if _is_plan_suite_path(path):
683
+ return OrchestratorReport(
684
+ passed=True,
685
+ path=str(path),
686
+ grep_count=0,
687
+ pass_count=0,
688
+ fail_count=0,
689
+ invocations=[],
690
+ )
691
+ if only is not None:
692
+ candidates = (only, f"{only}-grep")
693
+ modules = tuple(m for m in GREP_MODULES if m in candidates)
694
+ if not modules:
695
+ raise ValueError(f"unknown grep: {only!r}")
696
+ else:
697
+ modules = GREP_MODULES
698
+ invocations: list[GrepInvocation] = []
699
+ for module_name in modules:
700
+ start = time.perf_counter()
701
+ error: str | None = None
702
+ result: object = None
703
+ try:
704
+ check = _load_check(module_name)
705
+ result = check(content, path)
706
+ except Exception as exc: # noqa: BLE001, RUF100 - fail-open isolation boundary: one matcher's internal error (load failure or check() raise) must never fail-close the operator's write; the error is recorded on the invocation and surfaced, not swallowed silently
707
+ error = f"{type(exc).__name__}: {exc}"
708
+ elapsed = round(time.perf_counter() - start, 4)
709
+ if error is not None:
710
+ invocations.append(
711
+ GrepInvocation(
712
+ grep=module_name,
713
+ passed=True,
714
+ findings=[],
715
+ elapsed_seconds=elapsed,
716
+ over_budget=False,
717
+ error=error,
718
+ )
719
+ )
720
+ else:
721
+ invocations.append(
722
+ GrepInvocation(
723
+ grep=module_name,
724
+ passed=bool(getattr(result, "passed", False)),
725
+ findings=_findings_as_dicts(result),
726
+ elapsed_seconds=elapsed,
727
+ over_budget=elapsed > PER_GREP_BUDGET_SECONDS,
728
+ )
729
+ )
730
+ pass_count = sum(1 for i in invocations if i.passed)
731
+ fail_count = len(invocations) - pass_count
732
+ return OrchestratorReport(
733
+ passed=fail_count == 0,
734
+ path=str(path) if path is not None else None,
735
+ grep_count=len(invocations),
736
+ pass_count=pass_count,
737
+ fail_count=fail_count,
738
+ invocations=invocations,
739
+ )
740
+
741
+
742
+ _POSITION_KEYS: Final[frozenset[str]] = frozenset(
743
+ {
744
+ "line",
745
+ "lineno",
746
+ "line_number",
747
+ "start_line",
748
+ "end_line",
749
+ "column",
750
+ "col",
751
+ "pos",
752
+ "position",
753
+ "range",
754
+ "occurrences",
755
+ "context",
756
+ }
757
+ )
758
+
759
+
760
+ def _read_tool_input_from_stdin() -> tuple[str, Path | None, str | None]:
761
+ """Parse the harness tool-input JSON; extract post / path / pre."""
762
+ raw = sys.stdin.read()
763
+ if not raw.strip():
764
+ return "", None, None
765
+ try:
766
+ payload = json.loads(raw)
767
+ except json.JSONDecodeError:
768
+ return raw, None, None
769
+ tool_input = payload.get("tool_input") or {}
770
+ if not isinstance(tool_input, dict):
771
+ return raw, None, None
772
+ file_path_raw = tool_input.get("file_path")
773
+ path = (
774
+ Path(file_path_raw)
775
+ if isinstance(file_path_raw, str) and file_path_raw
776
+ else None
777
+ )
778
+ if "new_string" in tool_input and "old_string" in tool_input and path is not None:
779
+ try:
780
+ existing = path.read_text(encoding="utf-8")
781
+ new_str = tool_input.get("new_string") or ""
782
+ old_str = tool_input.get("old_string") or ""
783
+ replace_all = bool(tool_input.get("replace_all"))
784
+ if replace_all:
785
+ content = existing.replace(old_str, new_str)
786
+ else:
787
+ content = existing.replace(old_str, new_str, 1)
788
+ post = content if isinstance(content, str) else ""
789
+ return post, path, existing
790
+ except (FileNotFoundError, OSError, UnicodeDecodeError):
791
+ pass
792
+ content = tool_input.get("content") or tool_input.get("new_string") or ""
793
+ return content if isinstance(content, str) else "", path, None
794
+
795
+
796
+ def _finding_signature(finding: dict[str, object]) -> str:
797
+ """Canonicalise a finding for pre / post equality, ignoring position."""
798
+ canonical = {k: v for k, v in finding.items() if k not in _POSITION_KEYS}
799
+ return json.dumps(canonical, sort_keys=True, default=str)
800
+
801
+
802
+ def _diff_invocations(
803
+ pre: list[GrepInvocation], post: list[GrepInvocation]
804
+ ) -> list[GrepInvocation]:
805
+ """Return post invocations with pre-existing findings suppressed."""
806
+ pre_by_matcher: dict[str, list[str]] = {}
807
+ for inv in pre:
808
+ pre_by_matcher.setdefault(inv.grep, []).extend(
809
+ _finding_signature(f) for f in inv.findings
810
+ )
811
+ diffed = []
812
+ for inv in post:
813
+ bag = list(pre_by_matcher.get(inv.grep, ()))
814
+ retained = []
815
+ for finding in inv.findings:
816
+ sig = _finding_signature(finding)
817
+ if sig in bag:
818
+ bag.remove(sig)
819
+ continue
820
+ retained.append(finding)
821
+ diffed.append(
822
+ GrepInvocation(
823
+ grep=inv.grep,
824
+ passed=not retained,
825
+ findings=retained,
826
+ elapsed_seconds=inv.elapsed_seconds,
827
+ over_budget=inv.over_budget,
828
+ error=inv.error,
829
+ )
830
+ )
831
+ return diffed
832
+
833
+
834
+ def _orchestrator_diff_report(
835
+ pre_content: str,
836
+ post_content: str,
837
+ path: Path | None,
838
+ only: str | None,
839
+ ) -> OrchestratorReport:
840
+ """Run pre / post orchestrator passes; emit a differential report."""
841
+ pre_report = run_orchestrator(pre_content, path, only=only)
842
+ post_report = run_orchestrator(post_content, path, only=only)
843
+ diffed = _diff_invocations(
844
+ list(pre_report.invocations), list(post_report.invocations)
845
+ )
846
+ pass_count = sum(1 for inv in diffed if inv.passed)
847
+ fail_count = len(diffed) - pass_count
848
+ return OrchestratorReport(
849
+ passed=fail_count == 0,
850
+ path=post_report.path,
851
+ grep_count=len(diffed),
852
+ pass_count=pass_count,
853
+ fail_count=fail_count,
854
+ invocations=diffed,
855
+ )
856
+
857
+
858
+ def _read_cli_input(argv: list[str]) -> tuple[str, Path | None]:
859
+ """CLI mode — file path or `--stdin` direct content."""
860
+ if len(argv) >= 2 and argv[1] != STDIN_FLAG:
861
+ path = Path(argv[1])
862
+ return path.read_text(encoding="utf-8"), path
863
+ return sys.stdin.read(), None
864
+
865
+
866
+ def _split_check_flag(argv: list[str]) -> tuple[list[str], str | None]:
867
+ """Strip a leading ``--check <name>`` pair; return (rest, name or None).
868
+
869
+ The flag may appear at any leading position before the path or the
870
+ ``--stdin`` / ``--hook`` switch. When present, the next argv entry
871
+ is the grep name; both are consumed.
872
+ """
873
+ if len(argv) >= 3 and argv[1] == CHECK_FLAG:
874
+ return [argv[0], *argv[3:]], argv[2]
875
+ return argv, None
876
+
877
+
878
+ def _resolve_strict(argv: list[str]) -> tuple[list[str], bool]:
879
+ """Strip every ``--strict`` flag from *argv*; return (rest, strict_enabled).
880
+
881
+ The gate is advisory by default: findings are reported but never block,
882
+ abort, or force a non-zero exit. Strict mode is opt-in — the operator
883
+ enables it with the ``--strict`` flag or a truthy ``APOTHEM_CONFORMITY_STRICT``
884
+ environment variable (e.g., a CI job that wants findings to fail the build).
885
+ """
886
+ rest = [arg for arg in argv if arg != STRICT_FLAG]
887
+ flag_present = len(rest) != len(argv)
888
+ env_enabled = os.environ.get(STRICT_ENV, "").strip().lower() in _STRICT_TRUTHY
889
+ return rest, flag_present or env_enabled
890
+
891
+
892
+ def _gate_exit(passed: bool, *, strict: bool) -> int:
893
+ """Map a gate verdict to an exit code under the advisory-by-default posture.
894
+
895
+ A clean run always exits ``EXIT_PASS``. A run with findings exits
896
+ ``EXIT_PASS`` (advisory — the findings are reported, the action proceeds)
897
+ unless strict mode is enabled, in which case it exits ``EXIT_FAIL``.
898
+ """
899
+ if passed:
900
+ return EXIT_PASS
901
+ return EXIT_FAIL if strict else EXIT_PASS
902
+
903
+
904
+ def advisory_findings_present(payload: dict[str, object]) -> bool:
905
+ """Return the corpus / standalone payload's ``advisory_findings_present`` flag.
906
+
907
+ Both ``--all`` (standalone validators) and ``--all-perwrite`` (per-Write
908
+ corpus) emit this top-level boolean: True iff an advisory validator / matcher
909
+ reported a finding. The flag is the load-bearing handle a consumer reads to
910
+ surface advisory drift WITHOUT consulting the gating verdict; it is kept
911
+ separate from ``passed`` precisely because advisory drift is non-gating.
912
+ """
913
+ return bool(payload.get("advisory_findings_present", False))
914
+
915
+
916
+ def _strict_exit_with_advisory(
917
+ *,
918
+ blocking_passed: bool,
919
+ advisory_present: bool,
920
+ strict: bool,
921
+ ) -> int:
922
+ """Map a corpus / standalone verdict to an exit code, advisory flag in hand.
923
+
924
+ The EN-1 ratified posture (``.plans/apothem-overhaul/ws5-enforcement-posture.md``)
925
+ fixes the strict split: under ``--strict`` the BLOCKING verdict gates (a
926
+ blocking finding exits non-zero), and ADVISORY drift is surfaced but
927
+ **never gates** — a leaked reference-token / stale-AGENTS advisory does NOT
928
+ fail the strict step. This helper makes ``advisory_present`` load-bearing by
929
+ threading it through the strict decision explicitly: the gating exit code is
930
+ derived from ``blocking_passed`` alone, while ``advisory_present`` is
931
+ surfaced on stderr (so the drift is never silent) and intentionally does not
932
+ alter the exit code. The choice is pinned by a test so promotion of any
933
+ advisory member to gating is a deliberate, reviewed classification change,
934
+ never an accidental flip of the strict step's behavior.
935
+ """
936
+ if strict and advisory_present:
937
+ sys.stderr.write(
938
+ "conformity-gate: advisory finding(s) present (non-gating per the "
939
+ "EN-1 posture); surfaced for review, exit code unaffected\n"
940
+ )
941
+ return _gate_exit(blocking_passed, strict=strict)
942
+
943
+
944
+ def _resolve_validator(name: str) -> tuple[str, bool]:
945
+ """Resolve a short or full validator name to its canonical form.
946
+
947
+ Returns ``(canonical_name, is_standalone)`` or raises ``ValueError``
948
+ when the name is unknown. The short form drops the trailing
949
+ ``_grep`` suffix (e.g., ``file_header`` → ``file_header_grep``).
950
+ Accepts both hyphenated and underscored forms from CLI input.
951
+ """
952
+ normalized = name.replace("-", "_")
953
+ candidates = (normalized, f"{normalized}_grep")
954
+ # Normalize each registry entry to the underscored form before the
955
+ # membership test: GREP_MODULES entries are already underscored, but
956
+ # STANDALONE_MODULES entries are stored hyphenated (CLI ergonomics) and
957
+ # would never match the underscored *candidates* otherwise. The returned
958
+ # canonical preserves each registry's stored form.
959
+ for canonical in GREP_MODULES:
960
+ if canonical.replace("-", "_") in candidates:
961
+ return canonical, False
962
+ for canonical in STANDALONE_MODULES:
963
+ if canonical.replace("-", "_") in candidates:
964
+ return canonical, True
965
+ raise ValueError(f"unknown grep: {name!r}")
966
+
967
+
968
+ def _list_validators() -> str:
969
+ """Return the JSON enumeration of every registered validator."""
970
+ payload = {
971
+ "orchestrator": "conformity-gate",
972
+ "per_write_greps": list(GREP_MODULES),
973
+ "standalone_validators": list(STANDALONE_MODULES),
974
+ "total": len(GREP_MODULES) + len(STANDALONE_MODULES),
975
+ }
976
+ return json.dumps(payload, indent=2)
977
+
978
+
979
+ def _run_standalone(name: str, root: Path) -> tuple[bool, str]:
980
+ """Invoke a standalone validator via subprocess; return (passed, output).
981
+
982
+ The ``STANDALONE_MODULES`` names are hyphenated for CLI ergonomics
983
+ (``naming-grep``), but the on-disk module filenames are underscored
984
+ (``naming_grep.py``). Normalize the name to the underscored form
985
+ before resolving the script path so ``--all`` and ``--check`` both
986
+ locate the script regardless of which form the caller supplied.
987
+ """
988
+ script = TOOLS_DIR / f"{name.replace('-', '_')}.py"
989
+ if not script.exists():
990
+ return False, f"{name}: script absent at {script}"
991
+ try:
992
+ completed = subprocess.run( # noqa: S603 — trusted invocation: sys.executable + literal in-repo script path against a validated STANDALONE_MODULES name
993
+ [sys.executable, str(script), str(root)],
994
+ capture_output=True,
995
+ text=True,
996
+ check=False,
997
+ encoding="utf-8",
998
+ )
999
+ except OSError as exc:
1000
+ return False, f"{name}: invocation failed: {exc}"
1001
+ output = completed.stdout or completed.stderr or ""
1002
+ return completed.returncode == EXIT_PASS, output
1003
+
1004
+
1005
+ def _advisory_verdict(output: str) -> dict[str, object] | None:
1006
+ """Return an advisory validator's inner verdict from its JSON output.
1007
+
1008
+ An advisory validator (one whose JSON declares ``advisory: true``) exits 0
1009
+ even when it has findings, so its subprocess exit code — the gate's
1010
+ per-validator ``passed`` field — stays green while its JSON reports
1011
+ ``passed: false``. This parses that JSON so the inner verdict can be
1012
+ propagated into the gate report; a consumer reading the per-validator
1013
+ result then sees the drift without parsing the embedded ``output`` string.
1014
+
1015
+ Returns ``{"passed": bool, "findings": list}`` when the output is JSON
1016
+ declaring ``advisory: true``; otherwise None (the validator is not
1017
+ advisory, or its output is not parseable JSON).
1018
+ """
1019
+ try:
1020
+ payload = json.loads(output)
1021
+ except (json.JSONDecodeError, ValueError, TypeError):
1022
+ return None
1023
+ if not isinstance(payload, dict) or not payload.get("advisory"):
1024
+ return None
1025
+ findings = payload.get("findings")
1026
+ return {
1027
+ "passed": bool(payload.get("passed", True)),
1028
+ "findings": findings if isinstance(findings, list) else [],
1029
+ }
1030
+
1031
+
1032
+ def _run_all(root: Path) -> tuple[bool, str]:
1033
+ """Run every standalone validator; aggregate exit verdicts.
1034
+
1035
+ Per-Write greps are NOT invoked in --all mode because they require
1036
+ file-content input from the harness's tool-input JSON. The harness
1037
+ itself runs them per-Write via the --hook dispatch path; --all is
1038
+ the corpus-level counterpart for CI / Makefile invocations.
1039
+
1040
+ Advisory validators exit 0 even when they report findings, so the overall
1041
+ gate stays green; their inner verdict is surfaced per-validator as
1042
+ ``advisory`` / ``advisory_passed`` / ``findings`` and aggregated into the
1043
+ top-level ``advisory_findings_present`` flag so the drift is visible
1044
+ without parsing each validator's embedded ``output``.
1045
+ """
1046
+ results: list[dict[str, object]] = []
1047
+ overall_passed = True
1048
+ advisory_findings_present = False
1049
+ for name in STANDALONE_MODULES:
1050
+ passed, output = _run_standalone(name, root)
1051
+ if not passed:
1052
+ overall_passed = False
1053
+ entry: dict[str, object] = {
1054
+ "validator": name,
1055
+ "passed": passed,
1056
+ "output": output.strip(),
1057
+ }
1058
+ verdict = _advisory_verdict(output)
1059
+ if verdict is not None:
1060
+ entry["advisory"] = True
1061
+ entry["advisory_passed"] = verdict["passed"]
1062
+ entry["findings"] = verdict["findings"]
1063
+ if not verdict["passed"]:
1064
+ advisory_findings_present = True
1065
+ results.append(entry)
1066
+ payload = {
1067
+ "orchestrator": "conformity-gate",
1068
+ "mode": "all",
1069
+ "root": str(root),
1070
+ "passed": overall_passed,
1071
+ "advisory_findings_present": advisory_findings_present,
1072
+ "validator_count": len(STANDALONE_MODULES),
1073
+ "results": results,
1074
+ }
1075
+ return overall_passed, json.dumps(payload, indent=2)
1076
+
1077
+
1078
+ def _corpus_tracked_files(root: Path) -> list[Path]:
1079
+ """Return the git-tracked files under *root* as resolved absolute Paths.
1080
+
1081
+ Uses ``git ls-files`` so the corpus is exactly the tracked working tree —
1082
+ ignored artifacts (``dist/``, caches, ``.plans/`` ephemera) are excluded by
1083
+ construction. The invocation is read-only. When git is unavailable or
1084
+ *root* is not a work tree, returns an empty list (the caller treats an
1085
+ empty corpus as a clean pass — there is nothing tracked to scan).
1086
+ """
1087
+ try:
1088
+ completed = subprocess.run(
1089
+ ["git", "ls-files", "-z"], # noqa: S607 — read-only invocation; argv list (never shell); `git` resolved from PATH is the standard cross-platform invocation, mirroring the repo's other git subprocess call sites
1090
+ cwd=str(root),
1091
+ capture_output=True,
1092
+ check=False,
1093
+ encoding="utf-8",
1094
+ )
1095
+ except (OSError, ValueError):
1096
+ return []
1097
+ if completed.returncode != EXIT_PASS:
1098
+ return []
1099
+ rels = [entry for entry in completed.stdout.split("\0") if entry]
1100
+ return [(root / rel).resolve() for rel in rels]
1101
+
1102
+
1103
+ def _is_corpus_fixture_path(path: Path) -> bool:
1104
+ """Return True for deliberately-malformed conformity-test fixture files.
1105
+
1106
+ The ``tests/conformity/<matcher>/`` and ``tests/fixtures/`` trees carry
1107
+ ``fail.*`` / ``pass.*`` and other reference fixtures whose content is
1108
+ intentionally non-conformant (a planted violation a matcher's own unit test
1109
+ asserts on). Scanning them in corpus mode would surface those planted
1110
+ violations as corpus findings. They are exempt by the same principle the
1111
+ ``.plans/`` short-circuit applies: fixture data is not shipped codebase
1112
+ content held to the quality bar.
1113
+ """
1114
+ parts = path.parts
1115
+ if "tests" not in parts:
1116
+ return False
1117
+ tail = parts[parts.index("tests") + 1 :]
1118
+ return bool(tail) and tail[0] in {"conformity", "fixtures"}
1119
+
1120
+
1121
+ def _matcher_applies_to(module_name: str, path: Path) -> bool:
1122
+ """Return True when *module_name* should inspect *path* in corpus mode.
1123
+
1124
+ Honors the per-suffix applicability map for matchers whose rule is suffix-
1125
+ scoped but whose own ``check()`` does not gate by file type (so a Markdown
1126
+ file is never bare-except-scanned). Matchers absent from the map self-gate
1127
+ inside ``check()`` and are always invoked (they return clean for files
1128
+ their rule does not govern).
1129
+ """
1130
+ suffixes = _PER_SUFFIX_APPLICABILITY.get(module_name)
1131
+ if suffixes is None:
1132
+ return True
1133
+ return path.suffix.lower() in suffixes
1134
+
1135
+
1136
+ def _run_all_perwrite(root: Path) -> tuple[bool, str]:
1137
+ """Run every per-Write matcher over the git-tracked corpus under *root*.
1138
+
1139
+ The corpus counterpart to the harness's per-write hook dispatch: each
1140
+ tracked file is routed through every ``GREP_MODULES`` matcher with per-
1141
+ suffix applicability (``_matcher_applies_to``) and the same ``.plans/`` /
1142
+ fixture exemptions the per-write path honors. Findings aggregate per
1143
+ matcher and split into a blocking tally and an advisory tally per the
1144
+ ``_BLOCKING_PER_WRITE_GREPS`` / ``_ADVISORY_PER_WRITE_GREPS`` partition.
1145
+
1146
+ Returns ``(blocking_clean, payload)`` where ``blocking_clean`` is True iff
1147
+ zero blocking matchers flagged a finding. Advisory findings are reported
1148
+ (so drift is never silent) but never affect ``blocking_clean``. A matcher
1149
+ result carrying a ``note`` (EN-2's "scope not resolvable" skip) is not a
1150
+ finding. The caller maps ``blocking_clean`` to an exit code under the
1151
+ advisory-by-default posture (``--strict`` makes a non-clean blocking run
1152
+ exit non-zero).
1153
+ """
1154
+ files = _corpus_tracked_files(root)
1155
+ # matcher -> {"finding_count": int, "file_count": int, "files": [rel, ...]}
1156
+ blocking: dict[str, dict[str, object]] = {}
1157
+ advisory: dict[str, dict[str, object]] = {}
1158
+ files_scanned = 0
1159
+ for abs_path in files:
1160
+ if abs_path.suffix.lower() in _CORPUS_BINARY_SUFFIXES:
1161
+ continue
1162
+ if _is_plan_suite_path(abs_path) or _is_corpus_fixture_path(abs_path):
1163
+ continue
1164
+ if "_vendor" in abs_path.parts or "node_modules" in abs_path.parts:
1165
+ # Vendored / third-party trees carry upstream conventions, not the
1166
+ # apothem quality bar — exempt per schemas/header-exceptions.txt
1167
+ # (`**/_vendor/**`, `**/node_modules/**`).
1168
+ continue
1169
+ try:
1170
+ content = abs_path.read_text(encoding="utf-8")
1171
+ except (OSError, UnicodeDecodeError):
1172
+ # Undecodable-as-text tracked file (an unlisted binary suffix);
1173
+ # there is nothing for the text matchers to scan.
1174
+ continue
1175
+ files_scanned += 1
1176
+ try:
1177
+ rel = str(abs_path.relative_to(root))
1178
+ except ValueError:
1179
+ rel = str(abs_path)
1180
+ for module_name in GREP_MODULES:
1181
+ if not _matcher_applies_to(module_name, abs_path):
1182
+ continue
1183
+ try:
1184
+ result = _load_check(module_name)(content, abs_path)
1185
+ except Exception: # noqa: S112, BLE001, RUF100 — fail-open isolation: one matcher's internal error (load failure or check() raise) must never fail-close the corpus run; the matcher contributes no finding for this file and the run proceeds, mirroring run_orchestrator's per-matcher isolation boundary (BLE001 is the intent marker; RUF100 self-suppresses because ruff's BLE family is not active)
1186
+ continue
1187
+ if getattr(result, "note", None) is not None:
1188
+ # EN-2 scope-not-resolvable skip — not a finding.
1189
+ continue
1190
+ findings = getattr(result, "findings", None) or []
1191
+ if not findings:
1192
+ continue
1193
+ bucket = blocking if module_name in _BLOCKING_PER_WRITE_GREPS else advisory
1194
+ entry = bucket.setdefault(
1195
+ module_name,
1196
+ {"finding_count": 0, "file_count": 0, "files": []},
1197
+ )
1198
+ entry["finding_count"] = cast(int, entry["finding_count"]) + len(findings)
1199
+ entry["file_count"] = cast(int, entry["file_count"]) + 1
1200
+ sample = entry["files"]
1201
+ if isinstance(sample, list) and len(sample) < 10:
1202
+ sample.append(rel)
1203
+ blocking_clean = not blocking
1204
+ blocking_results = [
1205
+ {
1206
+ "matcher": name,
1207
+ "finding_count": data["finding_count"],
1208
+ "file_count": data["file_count"],
1209
+ "files": data["files"],
1210
+ }
1211
+ for name, data in sorted(blocking.items())
1212
+ ]
1213
+ advisory_results = [
1214
+ {
1215
+ "matcher": name,
1216
+ "finding_count": advisory[name]["finding_count"] if name in advisory else 0,
1217
+ "file_count": advisory[name]["file_count"] if name in advisory else 0,
1218
+ "files": advisory[name]["files"] if name in advisory else [],
1219
+ "reason": _ADVISORY_RATIONALE[name][0],
1220
+ "remediation_owner": _ADVISORY_RATIONALE[name][1],
1221
+ }
1222
+ for name in sorted(_ADVISORY_PER_WRITE_GREPS)
1223
+ if name in advisory
1224
+ ]
1225
+ payload = {
1226
+ "orchestrator": "conformity-gate",
1227
+ "mode": "all-perwrite",
1228
+ "root": str(root),
1229
+ "passed": blocking_clean,
1230
+ "files_scanned": files_scanned,
1231
+ "blocking_matcher_count": len(_BLOCKING_PER_WRITE_GREPS),
1232
+ "advisory_matcher_count": len(_ADVISORY_PER_WRITE_GREPS),
1233
+ "blocking_findings_present": not blocking_clean,
1234
+ "advisory_findings_present": bool(advisory),
1235
+ "blocking": blocking_results,
1236
+ "advisory": advisory_results,
1237
+ }
1238
+ return blocking_clean, json.dumps(payload, indent=2)
1239
+
1240
+
1241
+ def _silent_pass(path: Path | None) -> int:
1242
+ """Emit a silent pass-through report to stdout; return EXIT_PASS.
1243
+
1244
+ Shared by the hook-mode short-circuits (out-of-scope target,
1245
+ per-project harness runtime-state target) so the matcher chain is
1246
+ skipped without blocking the write.
1247
+ """
1248
+ report = OrchestratorReport(
1249
+ passed=True,
1250
+ path=str(path) if path else None,
1251
+ grep_count=0,
1252
+ pass_count=0,
1253
+ fail_count=0,
1254
+ )
1255
+ print(report.to_json())
1256
+ return EXIT_PASS
1257
+
1258
+
1259
+ def _findings_summary(report: OrchestratorReport, *, strict: bool) -> str:
1260
+ """Build the human-readable findings summary for the hook's stderr surface.
1261
+
1262
+ The harness shows a ``PreToolUse`` hook's stderr to the operator; the JSON
1263
+ report on stdout is machine-facing. This summary surfaces the findings so
1264
+ they are never silent. In advisory mode (the default) the write proceeds
1265
+ and the summary points the operator at the fix plus the ``--strict`` opt-in;
1266
+ in strict mode the summary names the block reason. One line per finding;
1267
+ the matcher name plus its issue and detail are surfaced so it is actionable.
1268
+ """
1269
+ target = report.path or "<stdin>"
1270
+ if strict:
1271
+ header = (
1272
+ f"conformity-gate (strict) flagged {report.fail_count} matcher(s) "
1273
+ f"on {target} and exits non-zero to fail the CI / pre-commit step. "
1274
+ f"The shipped PreToolUse hook runs advisory and does not pass "
1275
+ f"--strict, so a runtime write proceeds; strict gating bites at "
1276
+ f"merge time, not at the tool call."
1277
+ )
1278
+ else:
1279
+ header = (
1280
+ f"conformity-gate (advisory) flagged {report.fail_count} matcher(s) "
1281
+ f"on {target}; the write proceeds — review and address the findings, "
1282
+ f"or run with --strict (or APOTHEM_CONFORMITY_STRICT=1) to block."
1283
+ )
1284
+ lines = [header]
1285
+ for inv in report.invocations:
1286
+ if inv.passed:
1287
+ continue
1288
+ for finding in inv.findings:
1289
+ label = (
1290
+ finding.get("issue")
1291
+ or finding.get("value")
1292
+ or finding.get("form")
1293
+ or "finding"
1294
+ )
1295
+ detail = finding.get("detail") or finding.get("rule") or ""
1296
+ entry = f" - [{inv.grep}] {label}"
1297
+ if detail:
1298
+ entry += f": {detail}"
1299
+ lines.append(entry)
1300
+ return "\n".join(lines)
1301
+
1302
+
1303
+ def main(argv: list[str] | None = None) -> int:
1304
+ if argv is None:
1305
+ argv = sys.argv
1306
+ argv, strict = _resolve_strict(argv)
1307
+ if len(argv) >= 2 and argv[1] == LIST_FLAG:
1308
+ print(_list_validators())
1309
+ return EXIT_PASS
1310
+ if len(argv) >= 2 and argv[1] == ALL_PERWRITE_FLAG:
1311
+ root = Path(argv[2]) if len(argv) >= 3 else Path.cwd()
1312
+ passed, payload = _run_all_perwrite(root)
1313
+ print(payload)
1314
+ return _strict_exit_with_advisory(
1315
+ blocking_passed=passed,
1316
+ advisory_present=advisory_findings_present(json.loads(payload)),
1317
+ strict=strict,
1318
+ )
1319
+ if len(argv) >= 2 and argv[1] == ALL_FLAG:
1320
+ root = Path(argv[2]) if len(argv) >= 3 else Path.cwd()
1321
+ passed, payload = _run_all(root)
1322
+ print(payload)
1323
+ return _strict_exit_with_advisory(
1324
+ blocking_passed=passed,
1325
+ advisory_present=advisory_findings_present(json.loads(payload)),
1326
+ strict=strict,
1327
+ )
1328
+ argv, only = _split_check_flag(argv)
1329
+ # --check <name> may name a standalone; route via subprocess when so.
1330
+ if only is not None:
1331
+ try:
1332
+ canonical, is_standalone = _resolve_validator(only)
1333
+ except ValueError as exc:
1334
+ sys.stderr.write(f"{exc}\n")
1335
+ return EXIT_FAIL
1336
+ if is_standalone:
1337
+ root = Path(argv[1]) if len(argv) >= 2 else Path.cwd()
1338
+ passed, output = _run_standalone(canonical, root)
1339
+ print(output)
1340
+ return _gate_exit(passed, strict=strict)
1341
+ only = canonical
1342
+ pre_content: str | None = None
1343
+ if len(argv) >= 2 and argv[1] == "--hook":
1344
+ # Harness-dispatched hook mode: parse tool-input JSON from stdin.
1345
+ content, path, pre_content = _read_tool_input_from_stdin()
1346
+ # No resolvable target path: an empty or malformed payload (no
1347
+ # `tool_input.file_path`) carries no write to gate. Fail open rather
1348
+ # than run matchers against a path-less body — a hook invocation we
1349
+ # cannot attribute to a file is not a write the gate should block.
1350
+ if path is None:
1351
+ return _silent_pass(None)
1352
+ scopes = _resolve_scopes()
1353
+ # Scope-aware short-circuit: when the write target lives outside the
1354
+ # configured conformity scope, return a silent pass-through report
1355
+ # so the matchers do not block writes against unrelated workspaces.
1356
+ if not _path_in_any_scope(path, scopes):
1357
+ return _silent_pass(path)
1358
+ # Harness runtime-state short-circuit: the harness's own ``projects/``
1359
+ # (per-project state incl. project memory) and ``memory/`` (global
1360
+ # memory) subtrees are operator/harness-owned state, not apothem-managed
1361
+ # config. Skip the matcher chain so the operator's memory writes ---
1362
+ # provenance-less and frontmatter-less by the auto-memory convention ---
1363
+ # are not fail-closed.
1364
+ if _is_harness_state_path(path, scopes):
1365
+ return _silent_pass(path)
1366
+ else:
1367
+ content, path = _read_cli_input(argv)
1368
+ try:
1369
+ if pre_content is not None:
1370
+ report = _orchestrator_diff_report(pre_content, content, path, only)
1371
+ else:
1372
+ report = run_orchestrator(content, path, only=only)
1373
+ except ValueError as exc:
1374
+ sys.stderr.write(f"{exc}" + chr(10))
1375
+ return EXIT_FAIL
1376
+ print(report.to_json())
1377
+ if not report.passed:
1378
+ # Surface the findings on stderr so they are never silent. The harness
1379
+ # shows a PreToolUse hook's stderr to the operator; in advisory mode the
1380
+ # write proceeds, in strict mode the non-zero exit blocks it.
1381
+ sys.stderr.write(_findings_summary(report, strict=strict) + chr(10))
1382
+ return _gate_exit(report.passed, strict=strict)
1383
+
1384
+
1385
+ if __name__ == "__main__":
1386
+ sys.exit(main(sys.argv))