@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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/LICENSES/MIT.txt +18 -0
- package/LICENSES/PSF-2.0.txt +47 -0
- package/README.md +549 -0
- package/bin/README.md +37 -0
- package/bin/apothem.mjs +78 -0
- package/package.json +75 -0
- package/pyproject.toml +347 -0
- package/src/apothem/README.md +52 -0
- package/src/apothem/__init__.py +66 -0
- package/src/apothem/__main__.py +28 -0
- package/src/apothem/_vendor/.keep +0 -0
- package/src/apothem/_vendor/__init__.py +25 -0
- package/src/apothem/_vendor/attr/__init__.py +104 -0
- package/src/apothem/_vendor/attr/__init__.pyi +389 -0
- package/src/apothem/_vendor/attr/_cmp.py +160 -0
- package/src/apothem/_vendor/attr/_cmp.pyi +13 -0
- package/src/apothem/_vendor/attr/_compat.py +99 -0
- package/src/apothem/_vendor/attr/_config.py +31 -0
- package/src/apothem/_vendor/attr/_funcs.py +497 -0
- package/src/apothem/_vendor/attr/_make.py +3406 -0
- package/src/apothem/_vendor/attr/_next_gen.py +674 -0
- package/src/apothem/_vendor/attr/_typing_compat.pyi +15 -0
- package/src/apothem/_vendor/attr/_version_info.py +89 -0
- package/src/apothem/_vendor/attr/_version_info.pyi +9 -0
- package/src/apothem/_vendor/attr/converters.py +162 -0
- package/src/apothem/_vendor/attr/converters.pyi +19 -0
- package/src/apothem/_vendor/attr/exceptions.py +95 -0
- package/src/apothem/_vendor/attr/exceptions.pyi +17 -0
- package/src/apothem/_vendor/attr/filters.py +72 -0
- package/src/apothem/_vendor/attr/filters.pyi +6 -0
- package/src/apothem/_vendor/attr/py.typed +0 -0
- package/src/apothem/_vendor/attr/setters.py +79 -0
- package/src/apothem/_vendor/attr/setters.pyi +20 -0
- package/src/apothem/_vendor/attr/validators.py +750 -0
- package/src/apothem/_vendor/attr/validators.pyi +140 -0
- package/src/apothem/_vendor/attr.LICENSE +21 -0
- package/src/apothem/_vendor/attrs/__init__.py +72 -0
- package/src/apothem/_vendor/attrs/__init__.pyi +314 -0
- package/src/apothem/_vendor/attrs/converters.py +3 -0
- package/src/apothem/_vendor/attrs/exceptions.py +3 -0
- package/src/apothem/_vendor/attrs/filters.py +3 -0
- package/src/apothem/_vendor/attrs/py.typed +0 -0
- package/src/apothem/_vendor/attrs/setters.py +3 -0
- package/src/apothem/_vendor/attrs/validators.py +3 -0
- package/src/apothem/_vendor/attrs.LICENSE +21 -0
- package/src/apothem/_vendor/jsonschema/__init__.py +120 -0
- package/src/apothem/_vendor/jsonschema/__main__.py +6 -0
- package/src/apothem/_vendor/jsonschema/_format.py +546 -0
- package/src/apothem/_vendor/jsonschema/_keywords.py +449 -0
- package/src/apothem/_vendor/jsonschema/_legacy_keywords.py +449 -0
- package/src/apothem/_vendor/jsonschema/_types.py +204 -0
- package/src/apothem/_vendor/jsonschema/_typing.py +29 -0
- package/src/apothem/_vendor/jsonschema/_utils.py +355 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/__init__.py +5 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/const_vs_enum.py +30 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/contains.py +28 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/import_benchmark.py +31 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/issue232/issue.json +2653 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/issue232.py +25 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/json_schema_test_suite.py +12 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/nested_schemas.py +56 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/subcomponents.py +42 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/unused_registry.py +35 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/useless_applicator_schemas.py +106 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/useless_keywords.py +32 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/validator_creation.py +14 -0
- package/src/apothem/_vendor/jsonschema/cli.py +292 -0
- package/src/apothem/_vendor/jsonschema/exceptions.py +490 -0
- package/src/apothem/_vendor/jsonschema/protocols.py +230 -0
- package/src/apothem/_vendor/jsonschema/validators.py +1410 -0
- package/src/apothem/_vendor/jsonschema.LICENSE +19 -0
- package/src/apothem/_vendor/jsonschema_specifications/__init__.py +12 -0
- package/src/apothem/_vendor/jsonschema_specifications/_core.py +38 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/metaschema.json +42 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/applicator +56 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/content +17 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/core +57 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/format +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data +37 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/validation +98 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/metaschema.json +58 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/applicator +48 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/content +17 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/core +51 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data +37 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated +15 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/validation +98 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft3/metaschema.json +172 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft4/metaschema.json +149 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft6/metaschema.json +153 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft7/metaschema.json +166 -0
- package/src/apothem/_vendor/jsonschema_specifications.LICENSE +19 -0
- package/src/apothem/_vendor/referencing/__init__.py +7 -0
- package/src/apothem/_vendor/referencing/_attrs.py +31 -0
- package/src/apothem/_vendor/referencing/_attrs.pyi +21 -0
- package/src/apothem/_vendor/referencing/_core.py +739 -0
- package/src/apothem/_vendor/referencing/exceptions.py +165 -0
- package/src/apothem/_vendor/referencing/jsonschema.py +642 -0
- package/src/apothem/_vendor/referencing/py.typed +0 -0
- package/src/apothem/_vendor/referencing/retrieval.py +94 -0
- package/src/apothem/_vendor/referencing/typing.py +61 -0
- package/src/apothem/_vendor/referencing.LICENSE +19 -0
- package/src/apothem/_vendor/rpds/__init__.py +251 -0
- package/src/apothem/_vendor/typing_extensions.LICENSE +279 -0
- package/src/apothem/_vendor/typing_extensions.py +4317 -0
- package/src/apothem/_vendor/vendor.txt +22 -0
- package/src/apothem/_vendor/yaml/__init__.py +389 -0
- package/src/apothem/_vendor/yaml/composer.py +138 -0
- package/src/apothem/_vendor/yaml/constructor.py +748 -0
- package/src/apothem/_vendor/yaml/cyaml.py +100 -0
- package/src/apothem/_vendor/yaml/dumper.py +61 -0
- package/src/apothem/_vendor/yaml/emitter.py +1137 -0
- package/src/apothem/_vendor/yaml/error.py +74 -0
- package/src/apothem/_vendor/yaml/events.py +85 -0
- package/src/apothem/_vendor/yaml/loader.py +63 -0
- package/src/apothem/_vendor/yaml/nodes.py +48 -0
- package/src/apothem/_vendor/yaml/parser.py +588 -0
- package/src/apothem/_vendor/yaml/reader.py +185 -0
- package/src/apothem/_vendor/yaml/representer.py +388 -0
- package/src/apothem/_vendor/yaml/resolver.py +226 -0
- package/src/apothem/_vendor/yaml/scanner.py +1435 -0
- package/src/apothem/_vendor/yaml/serializer.py +110 -0
- package/src/apothem/_vendor/yaml/tokens.py +103 -0
- package/src/apothem/_vendor/yaml.LICENSE +20 -0
- package/src/apothem/agents/README.md +60 -0
- package/src/apothem/agents/codebase-explorer.md +91 -0
- package/src/apothem/agents/convention-auditor.md +93 -0
- package/src/apothem/agents/dependency-auditor.md +97 -0
- package/src/apothem/agents/fact-checker.md +84 -0
- package/src/apothem/agents/mcp-builder.md +86 -0
- package/src/apothem/agents/memory-auditor.md +93 -0
- package/src/apothem/agents/prompt-evaluator.md +87 -0
- package/src/apothem/agents/quality-gate.md +103 -0
- package/src/apothem/agents/refactor-surgeon.md +74 -0
- package/src/apothem/agents/research-scout.md +73 -0
- package/src/apothem/agents/security-scanner.md +83 -0
- package/src/apothem/agents/test-runner.md +84 -0
- package/src/apothem/audit/README.md +73 -0
- package/src/apothem/audit/_scan_lib.py +182 -0
- package/src/apothem/audit/analyze_graph.py +260 -0
- package/src/apothem/audit/build_capability_graph.py +607 -0
- package/src/apothem/audit/build_inventory.py +657 -0
- package/src/apothem/audit/build_plans_provenance.py +997 -0
- package/src/apothem/audit/check_links.py +389 -0
- package/src/apothem/audit/classify_artifacts.py +381 -0
- package/src/apothem/audit/deprecated-tokens.txt +10 -0
- package/src/apothem/audit/execute_plans_migration.py +491 -0
- package/src/apothem/audit/known-projects.txt +15 -0
- package/src/apothem/audit/render_capability_index.py +467 -0
- package/src/apothem/audit/render_inventory.py +405 -0
- package/src/apothem/audit/scan_ai_surfaces.py +1125 -0
- package/src/apothem/audit/scan_ai_surfaces_coarse.py +261 -0
- package/src/apothem/audit/scan_drift_features.py +143 -0
- package/src/apothem/audit/scan_frontmatter.py +293 -0
- package/src/apothem/audit/scan_header_coverage.py +1134 -0
- package/src/apothem/audit/scan_plan_leakage.py +540 -0
- package/src/apothem/audit/scan_plans_discipline.py +188 -0
- package/src/apothem/audit/scan_secrets_pii.py +245 -0
- package/src/apothem/audit/scan_stale_tokens.py +296 -0
- package/src/apothem/audit/synthesize_drift.py +205 -0
- package/src/apothem/benchmarks/README.md +33 -0
- package/src/apothem/benchmarks/__init__.py +3 -0
- package/src/apothem/benchmarks/bench_agents.py +63 -0
- package/src/apothem/benchmarks/bench_hooks.py +93 -0
- package/src/apothem/benchmarks/bench_install.py +58 -0
- package/src/apothem/benchmarks/bench_tests.py +93 -0
- package/src/apothem/benchmarks/bench_validate_ecosystem.py +84 -0
- package/src/apothem/cli/README.md +33 -0
- package/src/apothem/cli/__init__.py +229 -0
- package/src/apothem/cli/_cmd_completion.py +88 -0
- package/src/apothem/cli/_cmd_diff.py +181 -0
- package/src/apothem/cli/_cmd_doctor.py +143 -0
- package/src/apothem/cli/_cmd_harnesses.py +167 -0
- package/src/apothem/cli/_cmd_install.py +327 -0
- package/src/apothem/cli/_cmd_migrate_workspace.py +143 -0
- package/src/apothem/cli/_cmd_profile.py +341 -0
- package/src/apothem/cli/_cmd_status.py +180 -0
- package/src/apothem/cli/_cmd_uninstall.py +215 -0
- package/src/apothem/cli/_cmd_update.py +397 -0
- package/src/apothem/cli/_cmd_verify.py +194 -0
- package/src/apothem/cli/_common_flags.py +90 -0
- package/src/apothem/cli/_epilogs.py +296 -0
- package/src/apothem/cli/_helpers.py +857 -0
- package/src/apothem/cli/_json_formatter.py +21 -0
- package/src/apothem/cli/_materialize.py +376 -0
- package/src/apothem/cli/completions/apothem.bash +30 -0
- package/src/apothem/cli/completions/apothem.fish +19 -0
- package/src/apothem/cli/completions/apothem.ps1 +27 -0
- package/src/apothem/cli/completions/apothem.zsh +42 -0
- package/src/apothem/cli/reference_export.py +126 -0
- package/src/apothem/commands/README.md +125 -0
- package/src/apothem/commands/a11y-audit.md +203 -0
- package/src/apothem/commands/architecture-review.md +194 -0
- package/src/apothem/commands/audit.md +165 -0
- package/src/apothem/commands/code-audit.md +218 -0
- package/src/apothem/commands/code-review.md +193 -0
- package/src/apothem/commands/dependency-audit.md +209 -0
- package/src/apothem/commands/docs-review.md +199 -0
- package/src/apothem/commands/elevate.md +285 -0
- package/src/apothem/commands/eval.md +149 -0
- package/src/apothem/commands/fortress.md +172 -0
- package/src/apothem/commands/freshify.md +168 -0
- package/src/apothem/commands/github-deploy-fresh.md +178 -0
- package/src/apothem/commands/github-deploy-next.md +167 -0
- package/src/apothem/commands/perf-audit.md +198 -0
- package/src/apothem/commands/plan-amend.md +104 -0
- package/src/apothem/commands/plan-audit.md +127 -0
- package/src/apothem/commands/plan-design.md +257 -0
- package/src/apothem/commands/plan-execute.md +495 -0
- package/src/apothem/commands/plan-generate.md +351 -0
- package/src/apothem/commands/plan-review.md +555 -0
- package/src/apothem/commands/plan-spec.md +359 -0
- package/src/apothem/commands/plan-status.md +222 -0
- package/src/apothem/commands/plan.md +173 -0
- package/src/apothem/commands/projectify.md +142 -0
- package/src/apothem/commands/release-readiness.md +142 -0
- package/src/apothem/commands/research-analysis.md +241 -0
- package/src/apothem/commands/research-design.md +231 -0
- package/src/apothem/commands/research-disseminate.md +225 -0
- package/src/apothem/commands/research-experiment.md +232 -0
- package/src/apothem/commands/research-ideate.md +213 -0
- package/src/apothem/commands/research-paper.md +252 -0
- package/src/apothem/commands/research-proposal.md +220 -0
- package/src/apothem/commands/research-publish.md +255 -0
- package/src/apothem/commands/research-review.md +251 -0
- package/src/apothem/commands/research-sources.md +266 -0
- package/src/apothem/commands/research-spec.md +255 -0
- package/src/apothem/commands/research-synthesis.md +233 -0
- package/src/apothem/commands/research-theory.md +218 -0
- package/src/apothem/commands/research.md +181 -0
- package/src/apothem/commands/security-audit.md +196 -0
- package/src/apothem/commands/supply-chain-audit.md +192 -0
- package/src/apothem/commands/test-suite.md +146 -0
- package/src/apothem/commands/threat-model-audit.md +199 -0
- package/src/apothem/commands/ux-review.md +202 -0
- package/src/apothem/commands/workflow.md +162 -0
- package/src/apothem/conformity/README.md +173 -0
- package/src/apothem/conformity/__init__.py +1 -0
- package/src/apothem/conformity/_grep_base.py +93 -0
- package/src/apothem/conformity/agent_capability_grep.py +306 -0
- package/src/apothem/conformity/agents_md_coverage_grep.py +382 -0
- package/src/apothem/conformity/agnosticism_grep.py +311 -0
- package/src/apothem/conformity/always_on_budget_grep.py +318 -0
- package/src/apothem/conformity/bare_except_grep.py +115 -0
- package/src/apothem/conformity/binding_reciprocity_grep.py +151 -0
- package/src/apothem/conformity/brand_mark_grep.py +272 -0
- package/src/apothem/conformity/commented_out_code_grep.py +176 -0
- package/src/apothem/conformity/completion_claim_grep.py +169 -0
- package/src/apothem/conformity/conventional_commit_grep.py +319 -0
- package/src/apothem/conformity/copilot_instructions_presence_grep.py +324 -0
- package/src/apothem/conformity/cross_platform_matrix_grep.py +297 -0
- package/src/apothem/conformity/determinism_grep.py +306 -0
- package/src/apothem/conformity/diagram_staleness_grep.py +154 -0
- package/src/apothem/conformity/dynamism_grep.py +284 -0
- package/src/apothem/conformity/editorconfig_presence_grep.py +281 -0
- package/src/apothem/conformity/file_header_grep.py +502 -0
- package/src/apothem/conformity/freshness_token_grep.py +233 -0
- package/src/apothem/conformity/frontmatter_grep.py +274 -0
- package/src/apothem/conformity/frontmatter_value_grep.py +386 -0
- package/src/apothem/conformity/gate.py +1386 -0
- package/src/apothem/conformity/gitattributes_presence_grep.py +238 -0
- package/src/apothem/conformity/harden_runner_grep.py +320 -0
- package/src/apothem/conformity/hedging_grep.py +129 -0
- package/src/apothem/conformity/license_author_consistency_grep.py +204 -0
- package/src/apothem/conformity/link_check.py +327 -0
- package/src/apothem/conformity/magic_number_grep.py +182 -0
- package/src/apothem/conformity/multi_surface_coherence_grep.py +620 -0
- package/src/apothem/conformity/naming_grep.py +224 -0
- package/src/apothem/conformity/no_global_plans_grep.py +339 -0
- package/src/apothem/conformity/no_toplevel_docs_grep.py +120 -0
- package/src/apothem/conformity/oidc_trusted_publishing_grep.py +291 -0
- package/src/apothem/conformity/option_annotation_grep.py +352 -0
- package/src/apothem/conformity/orphan_output_grep.py +206 -0
- package/src/apothem/conformity/permissions_minimum_scope_grep.py +299 -0
- package/src/apothem/conformity/plain_language_grep.py +559 -0
- package/src/apothem/conformity/plan_next_step_consistency_grep.py +450 -0
- package/src/apothem/conformity/plan_suite_structure_grep.py +534 -0
- package/src/apothem/conformity/plans_discipline_language_grep.py +245 -0
- package/src/apothem/conformity/production_ready_pr_grep.py +200 -0
- package/src/apothem/conformity/recommend_next_step_grep.py +250 -0
- package/src/apothem/conformity/redundancy_grep.py +401 -0
- package/src/apothem/conformity/reference_token_grep.py +230 -0
- package/src/apothem/conformity/registry_capability_consistency_grep.py +368 -0
- package/src/apothem/conformity/secret_leak_grep.py +193 -0
- package/src/apothem/conformity/semver_stability_grep.py +358 -0
- package/src/apothem/conformity/smoke_install_grep.py +194 -0
- package/src/apothem/conformity/static_version_grep.py +284 -0
- package/src/apothem/conformity/token_efficiency_grep.py +185 -0
- package/src/apothem/conformity/unpinned_action_grep.py +115 -0
- package/src/apothem/conformity/user_confirm_grep.py +74 -0
- package/src/apothem/conformity/workflow_concurrency_grep.py +283 -0
- package/src/apothem/harnesses/README.md +63 -0
- package/src/apothem/harnesses/__init__.py +16 -0
- package/src/apothem/harnesses/_shared/README.md +36 -0
- package/src/apothem/harnesses/_shared/__init__.py +12 -0
- package/src/apothem/harnesses/_shared/install_driver.py +281 -0
- package/src/apothem/harnesses/_shared/install_driver_apply.py +612 -0
- package/src/apothem/harnesses/_shared/install_driver_backup.py +535 -0
- package/src/apothem/harnesses/_shared/install_driver_converters.py +310 -0
- package/src/apothem/harnesses/_shared/install_driver_lifecycle.py +495 -0
- package/src/apothem/harnesses/_shared/install_driver_materialize.py +675 -0
- package/src/apothem/harnesses/_shared/install_driver_merge.py +656 -0
- package/src/apothem/harnesses/_shared/install_driver_pathsafety.py +137 -0
- package/src/apothem/harnesses/_shared/install_driver_planvalidation.py +240 -0
- package/src/apothem/harnesses/_shared/install_driver_removal.py +366 -0
- package/src/apothem/harnesses/_shared/install_driver_treeops.py +248 -0
- package/src/apothem/harnesses/_shared/install_driver_types.py +330 -0
- package/src/apothem/harnesses/_shared/wrapper_factories.py +448 -0
- package/src/apothem/harnesses/antigravity/STANDARD-CONVENTION-PIN.md +91 -0
- package/src/apothem/harnesses/antigravity/__init__.py +70 -0
- package/src/apothem/harnesses/antigravity/capabilities.yml +40 -0
- package/src/apothem/harnesses/antigravity/install.py +63 -0
- package/src/apothem/harnesses/antigravity/templates/GEMINI.md +40 -0
- package/src/apothem/harnesses/antigravity/templates/plugin.json +5 -0
- package/src/apothem/harnesses/antigravity/uninstall.py +22 -0
- package/src/apothem/harnesses/antigravity/update.py +10 -0
- package/src/apothem/harnesses/antigravity/verify.py +11 -0
- package/src/apothem/harnesses/claude_code/STANDARD-CONVENTION-PIN.md +65 -0
- package/src/apothem/harnesses/claude_code/__init__.py +107 -0
- package/src/apothem/harnesses/claude_code/capabilities.yml +42 -0
- package/src/apothem/harnesses/claude_code/install.py +147 -0
- package/src/apothem/harnesses/claude_code/templates/settings.json +351 -0
- package/src/apothem/harnesses/claude_code/uninstall.py +23 -0
- package/src/apothem/harnesses/claude_code/update.py +10 -0
- package/src/apothem/harnesses/claude_code/verify.py +11 -0
- package/src/apothem/harnesses/codebuddy/STANDARD-CONVENTION-PIN.md +74 -0
- package/src/apothem/harnesses/codebuddy/__init__.py +49 -0
- package/src/apothem/harnesses/codebuddy/capabilities.yml +34 -0
- package/src/apothem/harnesses/codebuddy/install.py +40 -0
- package/src/apothem/harnesses/codebuddy/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/codebuddy/uninstall.py +25 -0
- package/src/apothem/harnesses/codebuddy/update.py +10 -0
- package/src/apothem/harnesses/codebuddy/verify.py +11 -0
- package/src/apothem/harnesses/codex/STANDARD-CONVENTION-PIN.md +79 -0
- package/src/apothem/harnesses/codex/__init__.py +72 -0
- package/src/apothem/harnesses/codex/capabilities.yml +40 -0
- package/src/apothem/harnesses/codex/install.py +69 -0
- package/src/apothem/harnesses/codex/templates/AGENTS.md +40 -0
- package/src/apothem/harnesses/codex/templates/hooks.json +127 -0
- package/src/apothem/harnesses/codex/uninstall.py +23 -0
- package/src/apothem/harnesses/codex/update.py +10 -0
- package/src/apothem/harnesses/codex/verify.py +11 -0
- package/src/apothem/harnesses/cursor/STANDARD-CONVENTION-PIN.md +79 -0
- package/src/apothem/harnesses/cursor/__init__.py +48 -0
- package/src/apothem/harnesses/cursor/capabilities.yml +42 -0
- package/src/apothem/harnesses/cursor/install.py +38 -0
- package/src/apothem/harnesses/cursor/templates/apothem-rules.mdc +40 -0
- package/src/apothem/harnesses/cursor/uninstall.py +25 -0
- package/src/apothem/harnesses/cursor/update.py +10 -0
- package/src/apothem/harnesses/cursor/verify.py +11 -0
- package/src/apothem/harnesses/gemini_cli/STANDARD-CONVENTION-PIN.md +102 -0
- package/src/apothem/harnesses/gemini_cli/__init__.py +52 -0
- package/src/apothem/harnesses/gemini_cli/capabilities.yml +43 -0
- package/src/apothem/harnesses/gemini_cli/install.py +43 -0
- package/src/apothem/harnesses/gemini_cli/templates/GEMINI.md +38 -0
- package/src/apothem/harnesses/gemini_cli/uninstall.py +25 -0
- package/src/apothem/harnesses/gemini_cli/update.py +10 -0
- package/src/apothem/harnesses/gemini_cli/verify.py +11 -0
- package/src/apothem/harnesses/github_copilot/STANDARD-CONVENTION-PIN.md +84 -0
- package/src/apothem/harnesses/github_copilot/__init__.py +47 -0
- package/src/apothem/harnesses/github_copilot/capabilities.yml +42 -0
- package/src/apothem/harnesses/github_copilot/install.py +40 -0
- package/src/apothem/harnesses/github_copilot/templates/copilot-instructions.md +33 -0
- package/src/apothem/harnesses/github_copilot/uninstall.py +25 -0
- package/src/apothem/harnesses/github_copilot/update.py +10 -0
- package/src/apothem/harnesses/github_copilot/verify.py +11 -0
- package/src/apothem/harnesses/glm/STANDARD-CONVENTION-PIN.md +77 -0
- package/src/apothem/harnesses/glm/__init__.py +56 -0
- package/src/apothem/harnesses/glm/capabilities.yml +33 -0
- package/src/apothem/harnesses/glm/install.py +45 -0
- package/src/apothem/harnesses/glm/templates/glm.toml +58 -0
- package/src/apothem/harnesses/glm/uninstall.py +25 -0
- package/src/apothem/harnesses/glm/update.py +10 -0
- package/src/apothem/harnesses/glm/verify.py +11 -0
- package/src/apothem/harnesses/hermes/STANDARD-CONVENTION-PIN.md +57 -0
- package/src/apothem/harnesses/hermes/__init__.py +33 -0
- package/src/apothem/harnesses/hermes/capabilities.yml +36 -0
- package/src/apothem/harnesses/hermes/install.py +17 -0
- package/src/apothem/harnesses/hermes/materializer.py +35 -0
- package/src/apothem/harnesses/hermes/uninstall.py +33 -0
- package/src/apothem/harnesses/hermes/update.py +10 -0
- package/src/apothem/harnesses/hermes/verify.py +11 -0
- package/src/apothem/harnesses/kimi_code/STANDARD-CONVENTION-PIN.md +128 -0
- package/src/apothem/harnesses/kimi_code/__init__.py +59 -0
- package/src/apothem/harnesses/kimi_code/capabilities.yml +40 -0
- package/src/apothem/harnesses/kimi_code/install.py +42 -0
- package/src/apothem/harnesses/kimi_code/templates/AGENTS.md +43 -0
- package/src/apothem/harnesses/kimi_code/uninstall.py +27 -0
- package/src/apothem/harnesses/kimi_code/update.py +10 -0
- package/src/apothem/harnesses/kimi_code/verify.py +11 -0
- package/src/apothem/harnesses/kiro/STANDARD-CONVENTION-PIN.md +77 -0
- package/src/apothem/harnesses/kiro/__init__.py +49 -0
- package/src/apothem/harnesses/kiro/capabilities.yml +36 -0
- package/src/apothem/harnesses/kiro/install.py +39 -0
- package/src/apothem/harnesses/kiro/templates/apothem-rules.md +36 -0
- package/src/apothem/harnesses/kiro/uninstall.py +25 -0
- package/src/apothem/harnesses/kiro/update.py +10 -0
- package/src/apothem/harnesses/kiro/verify.py +11 -0
- package/src/apothem/harnesses/open_claw/STANDARD-CONVENTION-PIN.md +62 -0
- package/src/apothem/harnesses/open_claw/__init__.py +35 -0
- package/src/apothem/harnesses/open_claw/capabilities.yml +35 -0
- package/src/apothem/harnesses/open_claw/install.py +17 -0
- package/src/apothem/harnesses/open_claw/materializer.py +36 -0
- package/src/apothem/harnesses/open_claw/uninstall.py +32 -0
- package/src/apothem/harnesses/open_claw/update.py +10 -0
- package/src/apothem/harnesses/open_claw/verify.py +11 -0
- package/src/apothem/harnesses/opencode/STANDARD-CONVENTION-PIN.md +76 -0
- package/src/apothem/harnesses/opencode/__init__.py +35 -0
- package/src/apothem/harnesses/opencode/capabilities.yml +43 -0
- package/src/apothem/harnesses/opencode/install.py +17 -0
- package/src/apothem/harnesses/opencode/materializer.py +31 -0
- package/src/apothem/harnesses/opencode/uninstall.py +34 -0
- package/src/apothem/harnesses/opencode/update.py +10 -0
- package/src/apothem/harnesses/opencode/verify.py +11 -0
- package/src/apothem/harnesses/qwen_code/STANDARD-CONVENTION-PIN.md +87 -0
- package/src/apothem/harnesses/qwen_code/__init__.py +37 -0
- package/src/apothem/harnesses/qwen_code/capabilities.yml +43 -0
- package/src/apothem/harnesses/qwen_code/install.py +19 -0
- package/src/apothem/harnesses/qwen_code/materializer.py +174 -0
- package/src/apothem/harnesses/qwen_code/templates/QWEN.md +30 -0
- package/src/apothem/harnesses/qwen_code/uninstall.py +34 -0
- package/src/apothem/harnesses/qwen_code/update.py +10 -0
- package/src/apothem/harnesses/qwen_code/verify.py +11 -0
- package/src/apothem/harnesses/trae/STANDARD-CONVENTION-PIN.md +70 -0
- package/src/apothem/harnesses/trae/__init__.py +49 -0
- package/src/apothem/harnesses/trae/capabilities.yml +34 -0
- package/src/apothem/harnesses/trae/install.py +38 -0
- package/src/apothem/harnesses/trae/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/trae/uninstall.py +25 -0
- package/src/apothem/harnesses/trae/update.py +10 -0
- package/src/apothem/harnesses/trae/verify.py +11 -0
- package/src/apothem/harnesses/windsurf/STANDARD-CONVENTION-PIN.md +91 -0
- package/src/apothem/harnesses/windsurf/__init__.py +52 -0
- package/src/apothem/harnesses/windsurf/capabilities.yml +40 -0
- package/src/apothem/harnesses/windsurf/install.py +41 -0
- package/src/apothem/harnesses/windsurf/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/windsurf/uninstall.py +25 -0
- package/src/apothem/harnesses/windsurf/update.py +10 -0
- package/src/apothem/harnesses/windsurf/verify.py +11 -0
- package/src/apothem/harnesses/zed/STANDARD-CONVENTION-PIN.md +92 -0
- package/src/apothem/harnesses/zed/__init__.py +57 -0
- package/src/apothem/harnesses/zed/capabilities.yml +38 -0
- package/src/apothem/harnesses/zed/install.py +41 -0
- package/src/apothem/harnesses/zed/templates/apothem-rules.md +32 -0
- package/src/apothem/harnesses/zed/uninstall.py +28 -0
- package/src/apothem/harnesses/zed/update.py +10 -0
- package/src/apothem/harnesses/zed/verify.py +11 -0
- package/src/apothem/hooks/README.md +81 -0
- package/src/apothem/hooks/__init__.py +24 -0
- package/src/apothem/hooks/askuserquestion_validator.py +380 -0
- package/src/apothem/hooks/dispatch.py +296 -0
- package/src/apothem/hooks/emit_hook_context.py +444 -0
- package/src/apothem/hooks/hooks.json +318 -0
- package/src/apothem/hooks/lib/README.md +39 -0
- package/src/apothem/hooks/lib/__init__.py +18 -0
- package/src/apothem/hooks/lib/bootstrap.ps1 +129 -0
- package/src/apothem/hooks/lib/bootstrap.sh +103 -0
- package/src/apothem/hooks/lib/events.py +51 -0
- package/src/apothem/hooks/lib/find-pwsh.ps1 +78 -0
- package/src/apothem/hooks/lib/find-pwsh.sh +76 -0
- package/src/apothem/hooks/lib/find-python.ps1 +63 -0
- package/src/apothem/hooks/lib/find-python.sh +97 -0
- package/src/apothem/hooks/lib/log.py +43 -0
- package/src/apothem/hooks/lib/resolve_root.py +264 -0
- package/src/apothem/hooks/messages/postcompact.md +14 -0
- package/src/apothem/hooks/messages/posttooluse-proactive-compaction.md +46 -0
- package/src/apothem/hooks/messages/precompact.md +14 -0
- package/src/apothem/hooks/messages/pretooluse-askuserquestion-recommended.md +65 -0
- package/src/apothem/hooks/messages/pretooluse-bash-plan-guard.md +97 -0
- package/src/apothem/hooks/messages/pretooluse-bash.md +39 -0
- package/src/apothem/hooks/messages/pretooluse-conformity.md +70 -0
- package/src/apothem/hooks/messages/pretooluse-dependency-guard.md +21 -0
- package/src/apothem/hooks/messages/pretooluse-edit-header-guard.md +61 -0
- package/src/apothem/hooks/messages/pretooluse-edit.md +21 -0
- package/src/apothem/hooks/messages/pretooluse-eval-guard.md +39 -0
- package/src/apothem/hooks/messages/pretooluse-notebookedit.md +11 -0
- package/src/apothem/hooks/messages/pretooluse-write-header-guard.md +45 -0
- package/src/apothem/hooks/messages/pretooluse-write-plan-guard.md +72 -0
- package/src/apothem/hooks/messages/pretooluse-write.md +21 -0
- package/src/apothem/hooks/messages/sessionstart.md +15 -0
- package/src/apothem/hooks/messages/stop.md +27 -0
- package/src/apothem/hooks/proactive_compaction_tracker.py +327 -0
- package/src/apothem/hooks/session_start_bootstrap.py +472 -0
- package/src/apothem/lib/README.md +42 -0
- package/src/apothem/lib/__init__.py +13 -0
- package/src/apothem/lib/atomic_io.py +189 -0
- package/src/apothem/lib/auditor.py +687 -0
- package/src/apothem/lib/clean_slate.py +396 -0
- package/src/apothem/lib/contexts.py +352 -0
- package/src/apothem/lib/data_home.py +255 -0
- package/src/apothem/lib/frontmatter.py +101 -0
- package/src/apothem/lib/harness_materializer.py +213 -0
- package/src/apothem/lib/harness_protocol.py +59 -0
- package/src/apothem/lib/harness_registry.py +282 -0
- package/src/apothem/lib/harness_registry_data.py +843 -0
- package/src/apothem/lib/install_ledger.py +347 -0
- package/src/apothem/lib/learning.py +540 -0
- package/src/apothem/lib/memory.py +347 -0
- package/src/apothem/lib/parallel_sweep.py +234 -0
- package/src/apothem/lib/plan_tiers.py +200 -0
- package/src/apothem/lib/plugin_bootstrap.py +132 -0
- package/src/apothem/lib/plugin_tree.py +599 -0
- package/src/apothem/lib/profile.py +755 -0
- package/src/apothem/lib/profile_projection.py +198 -0
- package/src/apothem/lib/propagation-manifest.yaml +878 -0
- package/src/apothem/lib/propagation.py +220 -0
- package/src/apothem/lib/python_resolver.py +189 -0
- package/src/apothem/lib/reporter.py +62 -0
- package/src/apothem/lib/workspace_migration.py +323 -0
- package/src/apothem/output-styles/README.md +41 -0
- package/src/apothem/output-styles/concise-engineer.md +49 -0
- package/src/apothem/output-styles/default-architect.md +52 -0
- package/src/apothem/output-styles/default.md +113 -0
- package/src/apothem/output-styles/forensic-auditor.md +63 -0
- package/src/apothem/py.typed +0 -0
- package/src/apothem/rules/README.md +121 -0
- package/src/apothem/rules/agent-capability-discipline-matrix.md +89 -0
- package/src/apothem/rules/agent-capability-discipline.md +78 -0
- package/src/apothem/rules/agent-orchestration-patterns.md +144 -0
- package/src/apothem/rules/agent-orchestration.md +65 -0
- package/src/apothem/rules/agents-md-convention.md +86 -0
- package/src/apothem/rules/agile-sprints-elements.md +135 -0
- package/src/apothem/rules/agile-sprints.md +64 -0
- package/src/apothem/rules/agnostic-posture-checklist.md +47 -0
- package/src/apothem/rules/agnostic-posture.md +48 -0
- package/src/apothem/rules/authoritative-referencing-quotation.md +50 -0
- package/src/apothem/rules/authoritative-referencing.md +66 -0
- package/src/apothem/rules/authority-inquiry-categories.md +58 -0
- package/src/apothem/rules/authority-inquiry.md +54 -0
- package/src/apothem/rules/auto-memory-topic-files.md +86 -0
- package/src/apothem/rules/auto-memory.md +67 -0
- package/src/apothem/rules/bidirectional-binding.md +123 -0
- package/src/apothem/rules/canonical-layout-reporting-tiers.md +212 -0
- package/src/apothem/rules/canonical-layout.md +60 -0
- package/src/apothem/rules/clean-architecture-layers.md +186 -0
- package/src/apothem/rules/clean-room-generation-protocols.md +124 -0
- package/src/apothem/rules/clean-room-generation.md +59 -0
- package/src/apothem/rules/code-craft-conventions.md +101 -0
- package/src/apothem/rules/code-craft-markdown.md +138 -0
- package/src/apothem/rules/code-craft-python.md +154 -0
- package/src/apothem/rules/code-craft-shell.md +192 -0
- package/src/apothem/rules/cognitive-identity-techniques.md +180 -0
- package/src/apothem/rules/cognitive-identity.md +81 -0
- package/src/apothem/rules/context-management-budget.md +46 -0
- package/src/apothem/rules/context-management-protocol.md +161 -0
- package/src/apothem/rules/context-management-scratch.md +128 -0
- package/src/apothem/rules/context-management.md +85 -0
- package/src/apothem/rules/definitiveness-virtues.md +67 -0
- package/src/apothem/rules/definitiveness.md +58 -0
- package/src/apothem/rules/determinism.md +81 -0
- package/src/apothem/rules/disclosure-ledger-markers.md +58 -0
- package/src/apothem/rules/disclosure-ledger.md +52 -0
- package/src/apothem/rules/dynamism.md +38 -0
- package/src/apothem/rules/etc-extension.md +57 -0
- package/src/apothem/rules/expertise-posture-elements.md +68 -0
- package/src/apothem/rules/expertise-posture.md +54 -0
- package/src/apothem/rules/freshness-facade.md +64 -0
- package/src/apothem/rules/harness-adapter-shape-schemas.md +162 -0
- package/src/apothem/rules/harness-adapter-shape.md +42 -0
- package/src/apothem/rules/host-discovery-manifests.md +50 -0
- package/src/apothem/rules/host-discovery.md +56 -0
- package/src/apothem/rules/i18n-discipline-locale-cohorts.md +120 -0
- package/src/apothem/rules/i18n-discipline.md +70 -0
- package/src/apothem/rules/interactive-questions-canonical-shapes.md +590 -0
- package/src/apothem/rules/interactive-questions-detail.md +41 -0
- package/src/apothem/rules/interactive-questions-sweep-matchers.md +184 -0
- package/src/apothem/rules/interactive-questions.md +89 -0
- package/src/apothem/rules/large-file-generation.md +112 -0
- package/src/apothem/rules/large-file-reading.md +59 -0
- package/src/apothem/rules/living-docs.md +85 -0
- package/src/apothem/rules/multi-agent-workflow.md +57 -0
- package/src/apothem/rules/operational-mandates-expanded.md +78 -0
- package/src/apothem/rules/operational-mandates.md +88 -0
- package/src/apothem/rules/option-annotation-form.md +60 -0
- package/src/apothem/rules/option-annotation.md +45 -0
- package/src/apothem/rules/own-voice-reimplementation.md +86 -0
- package/src/apothem/rules/performance-discipline.md +91 -0
- package/src/apothem/rules/persistent-conventions-vigilance-checklist.md +54 -0
- package/src/apothem/rules/persistent-conventions-vigilance.md +61 -0
- package/src/apothem/rules/plain-language.md +56 -0
- package/src/apothem/rules/planning-techniques.md +130 -0
- package/src/apothem/rules/pre-emission-gate-bars.md +86 -0
- package/src/apothem/rules/pre-emission-gate.md +54 -0
- package/src/apothem/rules/production-ready-prs-surfaces.md +162 -0
- package/src/apothem/rules/production-ready-prs.md +83 -0
- package/src/apothem/rules/propagation.md +63 -0
- package/src/apothem/rules/recommend-next-step.md +106 -0
- package/src/apothem/rules/refactoring-discipline.md +76 -0
- package/src/apothem/rules/session-closure.md +44 -0
- package/src/apothem/rules/sota-elevation-exemplars.md +76 -0
- package/src/apothem/rules/sota-elevation.md +52 -0
- package/src/apothem/rules/source-accessibility.md +58 -0
- package/src/apothem/rules/surgical-manipulation.md +48 -0
- package/src/apothem/rules/systemic-participation-relations.md +108 -0
- package/src/apothem/rules/systemic-participation.md +70 -0
- package/src/apothem/rules/ten-dimension-check-dimensions.md +52 -0
- package/src/apothem/rules/ten-dimension-check.md +59 -0
- package/src/apothem/rules/token-budget-discipline.md +81 -0
- package/src/apothem/rules/token-efficiency-rewrite-protocol.md +79 -0
- package/src/apothem/rules/token-efficiency-rewrite.md +77 -0
- package/src/apothem/rules/tool-use-discipline.md +48 -0
- package/src/apothem/rules/visual-leverage.md +102 -0
- package/src/apothem/schemas/NOTICE.md +9 -0
- package/src/apothem/schemas/README.md +104 -0
- package/src/apothem/schemas/__init__.py +176 -0
- package/src/apothem/schemas/advisory-finding.schema.json +111 -0
- package/src/apothem/schemas/agent.schema.json +106 -0
- package/src/apothem/schemas/authorship-header.txt +1 -0
- package/src/apothem/schemas/cohort-manifest.yaml +248 -0
- package/src/apothem/schemas/cohort-metadata-vocabulary.yaml +168 -0
- package/src/apothem/schemas/cohort.schema.json +113 -0
- package/src/apothem/schemas/command.schema.json +68 -0
- package/src/apothem/schemas/compatibility-matrix.yaml +432 -0
- package/src/apothem/schemas/context-fragment.schema.json +64 -0
- package/src/apothem/schemas/freshness-token-denylist.txt +51 -0
- package/src/apothem/schemas/handoff-manifest.yaml +353 -0
- package/src/apothem/schemas/header-exceptions.txt +141 -0
- package/src/apothem/schemas/header-visibility.yaml +39 -0
- package/src/apothem/schemas/learning-signal.schema.json +46 -0
- package/src/apothem/schemas/memory-record.schema.json +61 -0
- package/src/apothem/schemas/output-style.schema.json +40 -0
- package/src/apothem/schemas/plan.schema.json +51 -0
- package/src/apothem/schemas/plugin.schema.json +83 -0
- package/src/apothem/schemas/profile.example.yaml +70 -0
- package/src/apothem/schemas/profile.minimal.yaml +6 -0
- package/src/apothem/schemas/profile.schema.json +396 -0
- package/src/apothem/schemas/reference-token-denylist.txt +25 -0
- package/src/apothem/schemas/skill.schema.json +75 -0
- package/src/apothem/skills/README.md +93 -0
- package/src/apothem/skills/dependency-upgrade/SKILL.md +105 -0
- package/src/apothem/skills/dev-toolkit/SKILL.md +120 -0
- package/src/apothem/skills/diagram-authoring/SKILL.md +113 -0
- package/src/apothem/skills/document-authoring/SKILL.md +118 -0
- package/src/apothem/skills/ecosystem-audit/SKILL.md +108 -0
- package/src/apothem/skills/ecosystem-audit/references/audit-fortress.md +85 -0
- package/src/apothem/skills/ecosystem-audit/references/procedure.md +162 -0
- package/src/apothem/skills/eval-harness/SKILL.md +88 -0
- package/src/apothem/skills/incident-runbook/SKILL.md +92 -0
- package/src/apothem/skills/multi-source-research/SKILL.md +90 -0
- package/src/apothem/skills/plan-suite/SKILL.md +118 -0
- package/src/apothem/skills/plan-suite/master_template.md +1324 -0
- package/src/apothem/skills/projectify/SKILL.md +117 -0
- package/src/apothem/skills/prompt-engineering/SKILL.md +122 -0
- package/src/apothem/skills/refactor-extract/SKILL.md +85 -0
- package/src/apothem/skills/research-suite/SKILL.md +170 -0
- package/src/apothem/skills/research-suite/references/directory-structure.md +47 -0
- package/src/apothem/skills/research-suite/references/lifecycle.md +67 -0
- package/src/apothem/skills/research-suite/references/principal-investigator-framework.md +37 -0
- package/src/apothem/skills/research-suite/references/rigor-mandates.md +30 -0
- package/src/apothem/skills/research-suite/research_template.md +476 -0
- package/src/apothem/skills/secret-rotation/SKILL.md +87 -0
- package/src/apothem/skills/source-synthesis/SKILL.md +92 -0
- package/src/apothem/skills/surgical-guard/SKILL.md +118 -0
- package/src/apothem/skills/test-authoring/SKILL.md +85 -0
- package/src/apothem/skills/vuln-triage/SKILL.md +91 -0
- package/src/apothem/skills/workflow/SKILL.md +139 -0
- package/src/apothem/statuslines/README.md +26 -0
- package/src/apothem/statuslines/__init__.py +20 -0
- package/src/apothem/statuslines/conformity.json +5 -0
- package/src/apothem/statuslines/render.py +334 -0
- package/src/apothem/statuslines/statusline.md +50 -0
- package/src/apothem/templates/README.md +43 -0
- package/src/apothem/templates/agents-md-template.md +80 -0
- package/src/apothem/templates/consideration-log.md +39 -0
- package/src/apothem/templates/expertise-gap-log.md +56 -0
- package/src/apothem/templates/master-index-template.md +93 -0
- package/src/apothem/templates/potency-map.md +53 -0
- package/src/apothem/templates/preservation-audit.md +60 -0
- package/src/apothem/templates/question-resolution-audit.md +52 -0
- package/src/apothem/templates/trace-matrix-template.md +77 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Verify the canonical Plans Discipline language reaches every required surface.
|
|
4
|
+
|
|
5
|
+
Why this validator exists. Spec section 3 declares Plans Discipline as
|
|
6
|
+
multi-surface enforcement (defense-in-depth). Four surfaces MUST carry
|
|
7
|
+
the directive:
|
|
8
|
+
|
|
9
|
+
* ``AGENTS.md`` — the canonical project instruction surface.
|
|
10
|
+
* ``CLAUDE.md`` — the Claude Code mirror.
|
|
11
|
+
* ``src/apothem/output-styles/default.md`` — the ecosystem-default tonal floor.
|
|
12
|
+
* ``.github/copilot-instructions.md`` — the Copilot-side mirror.
|
|
13
|
+
|
|
14
|
+
Each surface MUST contain either (a) the byte-exact fixture sentence
|
|
15
|
+
at ``tests/fixtures/plans-discipline.txt``, or (b) a semantic-paraphrase
|
|
16
|
+
that covers the canonical token set with sufficient density. The
|
|
17
|
+
two-mode admission honors the multi-surface-coherence comparator's
|
|
18
|
+
semantic-equivalence-token approach: byte-exact reproduction is
|
|
19
|
+
strongest, paraphrase with token coverage is acceptable.
|
|
20
|
+
|
|
21
|
+
Detection strategy. The validator walks the four surfaces from
|
|
22
|
+
``--root`` (the repository root). Per surface, it first probes for
|
|
23
|
+
byte-exact fixture presence; on miss, it computes a token-coverage
|
|
24
|
+
score against the canonical token set. A coverage of ``COVERAGE_FLOOR``
|
|
25
|
+
(0.6) admits the surface; below the floor is a finding.
|
|
26
|
+
|
|
27
|
+
Exit semantics. Exits 0 when every surface passes; exits 2 on any
|
|
28
|
+
finding. The exit-2 convention matches the conformity-gate
|
|
29
|
+
orchestrator's EXIT_FAIL constant.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import sys
|
|
36
|
+
from dataclasses import asdict, dataclass, field
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Final
|
|
39
|
+
|
|
40
|
+
GREP_NAME: Final[str] = "plans-discipline-language-grep"
|
|
41
|
+
RULE_ANCHOR: Final[str] = "CLAUDE.md Plans Discipline (multi-surface language)"
|
|
42
|
+
|
|
43
|
+
EXIT_PASS: Final[int] = 0
|
|
44
|
+
EXIT_FAIL: Final[int] = 2
|
|
45
|
+
|
|
46
|
+
# Surfaces that MUST carry the Plans Discipline directive. Paths are
|
|
47
|
+
# relative to the repository root supplied at invocation.
|
|
48
|
+
REQUIRED_SURFACES: Final[tuple[str, ...]] = (
|
|
49
|
+
"AGENTS.md",
|
|
50
|
+
"CLAUDE.md",
|
|
51
|
+
"src/apothem/output-styles/default.md",
|
|
52
|
+
".github/copilot-instructions.md",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Path to the byte-exact fixture relative to the repository root.
|
|
56
|
+
FIXTURE_PATH: Final[str] = "tests/fixtures/plans-discipline.txt"
|
|
57
|
+
|
|
58
|
+
# Canonical token set the paraphrase tolerance scores against. Each entry is a
|
|
59
|
+
# tuple of accepted alternatives; the entry counts toward coverage when ANY of
|
|
60
|
+
# its alternatives appears (case-insensitive substring). The tokens capture the
|
|
61
|
+
# directive's load-bearing semantics: the canonical destination, the negative
|
|
62
|
+
# claim, the global ban, and the non-negotiability frame.
|
|
63
|
+
#
|
|
64
|
+
# The canonical-destination entry is the sole project-local plans location
|
|
65
|
+
# ``<project-root>/.apothem/plans/``; a legacy ``<project-root>/.plans/`` tree
|
|
66
|
+
# is no longer canonical — operators upgrade it via ``apothem migrate-workspace``.
|
|
67
|
+
CANONICAL_TOKENS: Final[tuple[tuple[str, ...], ...]] = (
|
|
68
|
+
("<project-root>/.apothem/plans/",),
|
|
69
|
+
("~/.codex/",),
|
|
70
|
+
("~/.claude/",),
|
|
71
|
+
("never",),
|
|
72
|
+
("global",),
|
|
73
|
+
("plans discipline",),
|
|
74
|
+
)
|
|
75
|
+
COVERAGE_FLOOR: Final[float] = 0.6
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Finding:
|
|
80
|
+
"""One surface that lacks the directive at admission strength."""
|
|
81
|
+
|
|
82
|
+
surface: str
|
|
83
|
+
detail: str
|
|
84
|
+
coverage: float
|
|
85
|
+
rule: str = RULE_ANCHOR
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class SurfaceVerdict:
|
|
90
|
+
"""Per-surface admission record (informational; survives passing surfaces)."""
|
|
91
|
+
|
|
92
|
+
surface: str
|
|
93
|
+
mode: str
|
|
94
|
+
coverage: float
|
|
95
|
+
admitted: bool
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class GrepResult:
|
|
100
|
+
"""Aggregated multi-surface sweep result."""
|
|
101
|
+
|
|
102
|
+
grep: str
|
|
103
|
+
root: str
|
|
104
|
+
fixture_present: bool
|
|
105
|
+
surfaces: list[SurfaceVerdict]
|
|
106
|
+
passed: bool
|
|
107
|
+
findings: list[Finding] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
def to_json(self) -> str:
|
|
110
|
+
payload = {
|
|
111
|
+
"grep": self.grep,
|
|
112
|
+
"root": self.root,
|
|
113
|
+
"fixture_present": self.fixture_present,
|
|
114
|
+
"surfaces": [asdict(s) for s in self.surfaces],
|
|
115
|
+
"passed": self.passed,
|
|
116
|
+
"findings": [asdict(f) for f in self.findings],
|
|
117
|
+
}
|
|
118
|
+
return json.dumps(payload, indent=2)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _read_fixture(root: Path) -> str | None:
|
|
122
|
+
"""Return the fixture body, or None when the fixture file is absent."""
|
|
123
|
+
fixture_file = root / FIXTURE_PATH
|
|
124
|
+
if not fixture_file.exists():
|
|
125
|
+
return None
|
|
126
|
+
return fixture_file.read_text(encoding="utf-8").strip()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _coverage(body: str, tokens: tuple[tuple[str, ...], ...]) -> float:
|
|
130
|
+
"""Fraction of token entries present in body (case-insensitive substring).
|
|
131
|
+
|
|
132
|
+
Each entry is a tuple of accepted alternatives; an entry counts when ANY of
|
|
133
|
+
its alternatives appears. The canonical-destination entry admits the sole
|
|
134
|
+
project-local ``<project-root>/.apothem/plans/`` phrasing.
|
|
135
|
+
"""
|
|
136
|
+
if not tokens:
|
|
137
|
+
return 1.0
|
|
138
|
+
lowered = body.lower()
|
|
139
|
+
matched = sum(
|
|
140
|
+
1
|
|
141
|
+
for alternatives in tokens
|
|
142
|
+
if any(alt.lower() in lowered for alt in alternatives)
|
|
143
|
+
)
|
|
144
|
+
return matched / len(tokens)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _verdict_for_surface(
|
|
148
|
+
surface_path: str,
|
|
149
|
+
body: str,
|
|
150
|
+
fixture_text: str | None,
|
|
151
|
+
) -> tuple[SurfaceVerdict, Finding | None]:
|
|
152
|
+
"""Score one surface; return its verdict and any finding."""
|
|
153
|
+
if fixture_text and fixture_text in body:
|
|
154
|
+
verdict = SurfaceVerdict(
|
|
155
|
+
surface=surface_path, mode="byte-exact", coverage=1.0, admitted=True
|
|
156
|
+
)
|
|
157
|
+
return verdict, None
|
|
158
|
+
coverage = _coverage(body, CANONICAL_TOKENS)
|
|
159
|
+
admitted = coverage >= COVERAGE_FLOOR
|
|
160
|
+
verdict = SurfaceVerdict(
|
|
161
|
+
surface=surface_path,
|
|
162
|
+
mode="paraphrase",
|
|
163
|
+
coverage=round(coverage, 3),
|
|
164
|
+
admitted=admitted,
|
|
165
|
+
)
|
|
166
|
+
if admitted:
|
|
167
|
+
return verdict, None
|
|
168
|
+
finding = Finding(
|
|
169
|
+
surface=surface_path,
|
|
170
|
+
detail=(
|
|
171
|
+
f"plans-discipline language insufficient: coverage "
|
|
172
|
+
f"{coverage:.2f} < floor {COVERAGE_FLOOR:.2f} and "
|
|
173
|
+
f"byte-exact fixture absent"
|
|
174
|
+
),
|
|
175
|
+
coverage=round(coverage, 3),
|
|
176
|
+
)
|
|
177
|
+
return verdict, finding
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check(root: Path) -> GrepResult:
|
|
181
|
+
"""Sweep the four required surfaces under root for the directive."""
|
|
182
|
+
fixture_text = _read_fixture(root)
|
|
183
|
+
fixture_present = fixture_text is not None
|
|
184
|
+
verdicts: list[SurfaceVerdict] = []
|
|
185
|
+
findings: list[Finding] = []
|
|
186
|
+
if not fixture_present:
|
|
187
|
+
findings.append(
|
|
188
|
+
Finding(
|
|
189
|
+
surface=FIXTURE_PATH,
|
|
190
|
+
detail=(
|
|
191
|
+
"byte-exact fixture absent at canonical path; "
|
|
192
|
+
"byte-exact admission disabled for every surface"
|
|
193
|
+
),
|
|
194
|
+
coverage=0.0,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
for surface_path in REQUIRED_SURFACES:
|
|
198
|
+
full_path = root / surface_path
|
|
199
|
+
if not full_path.exists():
|
|
200
|
+
verdicts.append(
|
|
201
|
+
SurfaceVerdict(
|
|
202
|
+
surface=surface_path,
|
|
203
|
+
mode="absent",
|
|
204
|
+
coverage=0.0,
|
|
205
|
+
admitted=False,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
findings.append(
|
|
209
|
+
Finding(
|
|
210
|
+
surface=surface_path,
|
|
211
|
+
detail="surface file absent at canonical path",
|
|
212
|
+
coverage=0.0,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
continue
|
|
216
|
+
body = full_path.read_text(encoding="utf-8")
|
|
217
|
+
verdict, finding = _verdict_for_surface(surface_path, body, fixture_text)
|
|
218
|
+
verdicts.append(verdict)
|
|
219
|
+
if finding is not None:
|
|
220
|
+
findings.append(finding)
|
|
221
|
+
return GrepResult(
|
|
222
|
+
grep=GREP_NAME,
|
|
223
|
+
root=str(root),
|
|
224
|
+
fixture_present=fixture_present,
|
|
225
|
+
surfaces=verdicts,
|
|
226
|
+
passed=not findings,
|
|
227
|
+
findings=findings,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _read_input(argv: list[str]) -> Path:
|
|
232
|
+
if len(argv) >= 2:
|
|
233
|
+
return Path(argv[1])
|
|
234
|
+
return Path.cwd()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _main(argv: list[str]) -> int:
|
|
238
|
+
root = _read_input(argv)
|
|
239
|
+
result = check(root)
|
|
240
|
+
print(result.to_json())
|
|
241
|
+
return EXIT_PASS if result.passed else EXIT_FAIL
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
sys.exit(_main(sys.argv))
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Flag commits whose change-set lacks the production-ready four-class shape.
|
|
4
|
+
|
|
5
|
+
Why this enforcement exists. The production-ready discipline M15 requires
|
|
6
|
+
every change touching public surface to ship in the same change-set with
|
|
7
|
+
tests, documentation, and a CHANGELOG entry. A code change without its
|
|
8
|
+
companions persists as half-finished — the follow-up never lands; the
|
|
9
|
+
release notes lie. The pre-emission gate's mechanical bar 15 (M15 production-ready) catches
|
|
10
|
+
change-sets where public-surface code was modified but the companion
|
|
11
|
+
classes (tests / docs / CHANGELOG) are absent or trivially populated.
|
|
12
|
+
|
|
13
|
+
Detection strategy. The grep inspects the staged diff via `git diff
|
|
14
|
+
--cached --name-only` and classifies every touched file into one of
|
|
15
|
+
four classes (code, tests, docs, changelog). When code-class files are
|
|
16
|
+
touched, the grep verifies at least one tests-class and at least one
|
|
17
|
+
docs-class file is also touched, plus the changelog-class file is
|
|
18
|
+
present in the change-set. Per-file invocation is a soft pass — the
|
|
19
|
+
grep operates at change-set granularity rather than per-file, so the
|
|
20
|
+
orchestrator's per-Write dispatch returns clean and the CLI mode
|
|
21
|
+
performs the substantive check.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from dataclasses import asdict, dataclass, field
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Final
|
|
33
|
+
|
|
34
|
+
# Per-class path patterns. Patterns are inclusive — a file matching
|
|
35
|
+
# multiple patterns counts toward every class it matches.
|
|
36
|
+
CODE_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
|
|
37
|
+
re.compile(r"^src/.+\.(?:py|ts|js|go|rs|rb|java|kt|swift)$"),
|
|
38
|
+
re.compile(r"^lib/.+\.(?:py|ts|js|go|rs)$"),
|
|
39
|
+
re.compile(r"^(?:rules|commands|agents|skills|hooks|tools)/.+\.(?:py|md)$"),
|
|
40
|
+
)
|
|
41
|
+
TEST_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
|
|
42
|
+
re.compile(r"^tests?/.+"),
|
|
43
|
+
re.compile(r".+/test_[A-Za-z0-9_]+\.py$"),
|
|
44
|
+
re.compile(r".+_test\.(?:py|go|rs)$"),
|
|
45
|
+
re.compile(r".+\.test\.(?:ts|js)$"),
|
|
46
|
+
)
|
|
47
|
+
DOCS_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
|
|
48
|
+
re.compile(r"^docs?/.+"),
|
|
49
|
+
re.compile(r"^README\.md$"),
|
|
50
|
+
re.compile(r"^CONTRIBUTING\.md$"),
|
|
51
|
+
)
|
|
52
|
+
CHANGELOG_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
|
|
53
|
+
re.compile(r"^CHANGELOG\.md$"),
|
|
54
|
+
re.compile(r"^CHANGES(?:\.md)?$"),
|
|
55
|
+
re.compile(r"^HISTORY\.md$"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Per-file invocation mode: when the grep is dispatched against a
|
|
59
|
+
# single file (orchestrator's PreToolUse path), return PASS without
|
|
60
|
+
# inspecting git. The substantive check fires at commit-time CLI use.
|
|
61
|
+
PER_FILE_PASS_MODE: Final[bool] = True
|
|
62
|
+
|
|
63
|
+
GREP_NAME: Final[str] = "production-ready-pr-grep"
|
|
64
|
+
RULE_ANCHOR: Final[str] = "M15 production-ready §Same-change-set"
|
|
65
|
+
EXIT_PASS: Final[int] = 0
|
|
66
|
+
EXIT_FAIL: Final[int] = 2
|
|
67
|
+
STDIN_FLAG: Final[str] = "--stdin"
|
|
68
|
+
STAGED_FLAG: Final[str] = "--staged"
|
|
69
|
+
|
|
70
|
+
# Subprocess timeout for the git invocation; the per-grep budget is
|
|
71
|
+
# `gate.PER_GREP_BUDGET_SECONDS` (~520ms) but the staged-diff check is a
|
|
72
|
+
# single git command that completes well under that ceiling on
|
|
73
|
+
# representative repositories.
|
|
74
|
+
GIT_TIMEOUT_SECONDS: Final[int] = 5
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class Finding:
|
|
79
|
+
issue: str
|
|
80
|
+
detail: str
|
|
81
|
+
rule: str = RULE_ANCHOR
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class GrepResult:
|
|
86
|
+
grep: str
|
|
87
|
+
path: str | None
|
|
88
|
+
passed: bool
|
|
89
|
+
findings: list[Finding] = field(default_factory=list)
|
|
90
|
+
|
|
91
|
+
def to_json(self) -> str:
|
|
92
|
+
payload = {
|
|
93
|
+
"grep": self.grep,
|
|
94
|
+
"path": self.path,
|
|
95
|
+
"passed": self.passed,
|
|
96
|
+
"findings": [asdict(f) for f in self.findings],
|
|
97
|
+
}
|
|
98
|
+
return json.dumps(payload, indent=2)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _classify(path: str) -> set[str]:
|
|
102
|
+
"""Return the set of class labels the path matches."""
|
|
103
|
+
classes: set[str] = set()
|
|
104
|
+
if any(p.match(path) for p in CODE_PATTERNS):
|
|
105
|
+
classes.add("code")
|
|
106
|
+
if any(p.match(path) for p in TEST_PATTERNS):
|
|
107
|
+
classes.add("tests")
|
|
108
|
+
if any(p.match(path) for p in DOCS_PATTERNS):
|
|
109
|
+
classes.add("docs")
|
|
110
|
+
if any(p.match(path) for p in CHANGELOG_PATTERNS):
|
|
111
|
+
classes.add("changelog")
|
|
112
|
+
return classes
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _staged_paths() -> list[str]:
|
|
116
|
+
"""Return the staged-diff path list via git; empty on failure."""
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["git", "diff", "--cached", "--name-only"], # noqa: S607 # PATH-resolved git is acceptable for a developer-tool grep; full-path resolution would require host-discovery and breaks portability across operator setups.
|
|
120
|
+
check=True,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=GIT_TIMEOUT_SECONDS,
|
|
124
|
+
)
|
|
125
|
+
except (
|
|
126
|
+
subprocess.CalledProcessError,
|
|
127
|
+
subprocess.TimeoutExpired,
|
|
128
|
+
FileNotFoundError,
|
|
129
|
+
):
|
|
130
|
+
return []
|
|
131
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _check_staged() -> GrepResult:
|
|
135
|
+
"""Substantive check: inspect the staged diff against the four-class shape."""
|
|
136
|
+
paths = _staged_paths()
|
|
137
|
+
classes_present: set[str] = set()
|
|
138
|
+
for path in paths:
|
|
139
|
+
classes_present |= _classify(path)
|
|
140
|
+
findings: list[Finding] = []
|
|
141
|
+
if "code" in classes_present:
|
|
142
|
+
if "tests" not in classes_present:
|
|
143
|
+
findings.append(
|
|
144
|
+
Finding(
|
|
145
|
+
issue="missing tests-class file",
|
|
146
|
+
detail="code-class file modified; no tests/* file in the change-set",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
if "docs" not in classes_present:
|
|
150
|
+
findings.append(
|
|
151
|
+
Finding(
|
|
152
|
+
issue="missing docs-class file",
|
|
153
|
+
detail="code-class file modified; no docs/* / README / CONTRIBUTING in the change-set",
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
if "changelog" not in classes_present:
|
|
157
|
+
findings.append(
|
|
158
|
+
Finding(
|
|
159
|
+
issue="missing CHANGELOG entry",
|
|
160
|
+
detail="code-class file modified; CHANGELOG.md is not part of the change-set",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
return GrepResult(
|
|
164
|
+
grep=GREP_NAME,
|
|
165
|
+
path="<staged diff>",
|
|
166
|
+
passed=not findings,
|
|
167
|
+
findings=findings,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check(content: str, path: Path | None = None) -> GrepResult:
|
|
172
|
+
"""Per-file mode returns PASS; the substantive check fires via --staged."""
|
|
173
|
+
if PER_FILE_PASS_MODE:
|
|
174
|
+
return GrepResult(
|
|
175
|
+
grep=GREP_NAME,
|
|
176
|
+
path=str(path) if path is not None else None,
|
|
177
|
+
passed=True,
|
|
178
|
+
)
|
|
179
|
+
return _check_staged()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _read_input(argv: list[str]) -> tuple[str, Path | None]:
|
|
183
|
+
if len(argv) >= 2 and argv[1] != STDIN_FLAG:
|
|
184
|
+
path = Path(argv[1])
|
|
185
|
+
return path.read_text(encoding="utf-8"), path
|
|
186
|
+
return sys.stdin.read(), None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _main(argv: list[str]) -> int:
|
|
190
|
+
if len(argv) >= 2 and argv[1] == STAGED_FLAG:
|
|
191
|
+
result = _check_staged()
|
|
192
|
+
else:
|
|
193
|
+
content, path = _read_input(argv)
|
|
194
|
+
result = check(content, path)
|
|
195
|
+
print(result.to_json())
|
|
196
|
+
return EXIT_PASS if result.passed else EXIT_FAIL
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
sys.exit(_main(sys.argv))
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Verify every command file carries a `## Recommended Next Step` block.
|
|
4
|
+
|
|
5
|
+
Why this enforcement exists. Per `rules/recommend-next-step.md`, every
|
|
6
|
+
`commands/*.md` artifact terminates its working trace with a
|
|
7
|
+
Recommended-Next-Step block so the operator's onward path is named at
|
|
8
|
+
the close of every command. The block lands in one of two canonical
|
|
9
|
+
forms: the singular `## Recommended Next Step` heading followed by
|
|
10
|
+
substantive prose, or the multi-action `## Next Steps` heading whose
|
|
11
|
+
option list carries exactly one `**Recommended**` marker.
|
|
12
|
+
|
|
13
|
+
Detection strategy. The validator walks two terminal-surface classes —
|
|
14
|
+
`src/apothem/commands/*.md` (excluding `README.md` — the command-class
|
|
15
|
+
index is not itself a command) and `src/apothem/skills/*/SKILL.md` — to
|
|
16
|
+
match the rule's declared command-and-skill scope. For each surface it
|
|
17
|
+
locates either of the two heading forms; absence is a finding. For the
|
|
18
|
+
singular form the validator verifies the heading is followed by at least
|
|
19
|
+
one non-empty prose line. For the multi-action form the validator counts
|
|
20
|
+
`**Recommended**` markers between the heading and the next sibling H2 (or
|
|
21
|
+
end of file); exactly one is admissible. Zero or two-plus markers is a
|
|
22
|
+
finding.
|
|
23
|
+
|
|
24
|
+
Exit semantics. Exits 0 when every command file passes; exits 2 on any
|
|
25
|
+
finding. The exit-2 convention matches the conformity-gate
|
|
26
|
+
orchestrator's EXIT_FAIL constant.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
from dataclasses import asdict, dataclass, field
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Final
|
|
37
|
+
|
|
38
|
+
GREP_NAME: Final[str] = "recommend-next-step-grep"
|
|
39
|
+
RULE_ANCHOR: Final[str] = "rules/recommend-next-step.md"
|
|
40
|
+
|
|
41
|
+
EXIT_PASS: Final[int] = 0
|
|
42
|
+
EXIT_FAIL: Final[int] = 2
|
|
43
|
+
|
|
44
|
+
# Terminal-surface roots relative to the repository root supplied at invocation.
|
|
45
|
+
COMMANDS_DIR: Final[str] = "src/apothem/commands"
|
|
46
|
+
SKILLS_DIR: Final[str] = "src/apothem/skills"
|
|
47
|
+
|
|
48
|
+
# Filenames excluded from the sweep — folder-companion docs, not commands:
|
|
49
|
+
# the human-facing directory index and the agent-facing companion.
|
|
50
|
+
EXCLUDED_FILES: Final[frozenset[str]] = frozenset({"README.md", "AGENTS.md"})
|
|
51
|
+
|
|
52
|
+
# Heading patterns. The singular form is the dominant convention; the
|
|
53
|
+
# multi-action `## Next Steps` form is admissible when the command's
|
|
54
|
+
# close branches across several options.
|
|
55
|
+
SINGULAR_HEADING_RE: Final[re.Pattern[str]] = re.compile(
|
|
56
|
+
r"^##\s+Recommended\s+Next\s+Step\s*$"
|
|
57
|
+
)
|
|
58
|
+
MULTI_HEADING_RE: Final[re.Pattern[str]] = re.compile(r"^##\s+Next\s+Steps\s*$")
|
|
59
|
+
|
|
60
|
+
# Any H2 heading — used to scope the multi-action block's option list to
|
|
61
|
+
# the region between its own heading and the next sibling H2.
|
|
62
|
+
ANY_H2_RE: Final[re.Pattern[str]] = re.compile(r"^##\s+\S")
|
|
63
|
+
|
|
64
|
+
# Recommended marker. The convention is the literal `**Recommended**`
|
|
65
|
+
# bolded token, matching the canonical option-annotation discipline.
|
|
66
|
+
RECOMMENDED_MARKER_RE: Final[re.Pattern[str]] = re.compile(r"\*\*Recommended\*\*")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Finding:
|
|
71
|
+
"""One command file that lacks a valid Recommended-Next-Step block."""
|
|
72
|
+
|
|
73
|
+
surface: str
|
|
74
|
+
detail: str
|
|
75
|
+
rule: str = RULE_ANCHOR
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class GrepResult:
|
|
80
|
+
"""Aggregated sweep result across every command file."""
|
|
81
|
+
|
|
82
|
+
grep: str
|
|
83
|
+
root: str
|
|
84
|
+
files_inspected: int
|
|
85
|
+
passed: bool
|
|
86
|
+
findings: list[Finding] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
def to_json(self) -> str:
|
|
89
|
+
payload = {
|
|
90
|
+
"grep": self.grep,
|
|
91
|
+
"root": self.root,
|
|
92
|
+
"files_inspected": self.files_inspected,
|
|
93
|
+
"passed": self.passed,
|
|
94
|
+
"findings": [asdict(f) for f in self.findings],
|
|
95
|
+
}
|
|
96
|
+
return json.dumps(payload, indent=2)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _has_substantive_prose(lines: list[str], start_index: int) -> bool:
|
|
100
|
+
"""Return True iff at least one non-empty prose line follows the heading.
|
|
101
|
+
|
|
102
|
+
The scan stops at the next H2 (sibling heading) or end of file.
|
|
103
|
+
Empty lines, whitespace-only lines, and HTML-comment lines do not
|
|
104
|
+
count as substantive content.
|
|
105
|
+
"""
|
|
106
|
+
for line in lines[start_index + 1 :]:
|
|
107
|
+
if ANY_H2_RE.match(line):
|
|
108
|
+
return False
|
|
109
|
+
stripped = line.strip()
|
|
110
|
+
if not stripped:
|
|
111
|
+
continue
|
|
112
|
+
if stripped.startswith("<!--") or stripped.startswith("-->"):
|
|
113
|
+
continue
|
|
114
|
+
return True
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _count_recommended_markers(lines: list[str], start_index: int) -> int:
|
|
119
|
+
"""Count `**Recommended**` markers between heading and next H2."""
|
|
120
|
+
count = 0
|
|
121
|
+
for line in lines[start_index + 1 :]:
|
|
122
|
+
if ANY_H2_RE.match(line):
|
|
123
|
+
break
|
|
124
|
+
count += len(RECOMMENDED_MARKER_RE.findall(line))
|
|
125
|
+
return count
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _classify_command_file(path: Path) -> Finding | None:
|
|
129
|
+
"""Return a finding when the command file lacks a valid block."""
|
|
130
|
+
try:
|
|
131
|
+
body = path.read_text(encoding="utf-8")
|
|
132
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
133
|
+
return Finding(
|
|
134
|
+
surface=str(path),
|
|
135
|
+
detail=f"could not read command file: {exc}",
|
|
136
|
+
)
|
|
137
|
+
lines = body.splitlines()
|
|
138
|
+
singular_index = next(
|
|
139
|
+
(i for i, line in enumerate(lines) if SINGULAR_HEADING_RE.match(line)),
|
|
140
|
+
None,
|
|
141
|
+
)
|
|
142
|
+
multi_index = next(
|
|
143
|
+
(i for i, line in enumerate(lines) if MULTI_HEADING_RE.match(line)),
|
|
144
|
+
None,
|
|
145
|
+
)
|
|
146
|
+
if singular_index is None and multi_index is None:
|
|
147
|
+
return Finding(
|
|
148
|
+
surface=str(path),
|
|
149
|
+
detail=(
|
|
150
|
+
"no `## Recommended Next Step` or `## Next Steps` heading "
|
|
151
|
+
"found; the command must close with the canonical block"
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
if singular_index is not None:
|
|
155
|
+
if not _has_substantive_prose(lines, singular_index):
|
|
156
|
+
return Finding(
|
|
157
|
+
surface=str(path),
|
|
158
|
+
detail=(
|
|
159
|
+
"`## Recommended Next Step` heading present but block is "
|
|
160
|
+
"empty; the canonical form carries at least one prose line"
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
return None
|
|
164
|
+
# Multi-action form: verify exactly one Recommended marker.
|
|
165
|
+
# singular_index is None here (else returned above) and not both were None,
|
|
166
|
+
# so multi_index is necessarily set; the guard narrows the type and is
|
|
167
|
+
# unreachable in practice.
|
|
168
|
+
if multi_index is None: # pragma: no cover
|
|
169
|
+
return None
|
|
170
|
+
marker_count = _count_recommended_markers(lines, multi_index)
|
|
171
|
+
if marker_count == 0:
|
|
172
|
+
return Finding(
|
|
173
|
+
surface=str(path),
|
|
174
|
+
detail=(
|
|
175
|
+
"`## Next Steps` block carries zero `**Recommended**` markers; "
|
|
176
|
+
"the multi-action form requires exactly one"
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
if marker_count > 1:
|
|
180
|
+
return Finding(
|
|
181
|
+
surface=str(path),
|
|
182
|
+
detail=(
|
|
183
|
+
f"`## Next Steps` block carries {marker_count} `**Recommended**` "
|
|
184
|
+
f"markers; the multi-action form admits exactly one"
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def check(root: Path) -> GrepResult:
|
|
191
|
+
"""Sweep every command and skill surface under root for the block."""
|
|
192
|
+
findings: list[Finding] = []
|
|
193
|
+
files_inspected = 0
|
|
194
|
+
|
|
195
|
+
commands_dir = root / COMMANDS_DIR
|
|
196
|
+
if not commands_dir.is_dir():
|
|
197
|
+
findings.append(
|
|
198
|
+
Finding(
|
|
199
|
+
surface=COMMANDS_DIR,
|
|
200
|
+
detail=(f"commands directory absent at canonical path {commands_dir}"),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
for path in sorted(commands_dir.glob("*.md")):
|
|
205
|
+
if path.name in EXCLUDED_FILES:
|
|
206
|
+
continue
|
|
207
|
+
files_inspected += 1
|
|
208
|
+
finding = _classify_command_file(path)
|
|
209
|
+
if finding is not None:
|
|
210
|
+
findings.append(finding)
|
|
211
|
+
|
|
212
|
+
skills_dir = root / SKILLS_DIR
|
|
213
|
+
if not skills_dir.is_dir():
|
|
214
|
+
findings.append(
|
|
215
|
+
Finding(
|
|
216
|
+
surface=SKILLS_DIR,
|
|
217
|
+
detail=(f"skills directory absent at canonical path {skills_dir}"),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
for path in sorted(skills_dir.glob("*/SKILL.md")):
|
|
222
|
+
files_inspected += 1
|
|
223
|
+
finding = _classify_command_file(path)
|
|
224
|
+
if finding is not None:
|
|
225
|
+
findings.append(finding)
|
|
226
|
+
|
|
227
|
+
return GrepResult(
|
|
228
|
+
grep=GREP_NAME,
|
|
229
|
+
root=str(root),
|
|
230
|
+
files_inspected=files_inspected,
|
|
231
|
+
passed=not findings,
|
|
232
|
+
findings=findings,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _read_input(argv: list[str]) -> Path:
|
|
237
|
+
if len(argv) >= 2:
|
|
238
|
+
return Path(argv[1])
|
|
239
|
+
return Path.cwd()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _main(argv: list[str]) -> int:
|
|
243
|
+
root = _read_input(argv)
|
|
244
|
+
result = check(root)
|
|
245
|
+
print(result.to_json())
|
|
246
|
+
return EXIT_PASS if result.passed else EXIT_FAIL
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(_main(sys.argv))
|