@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,857 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Shared helpers, constants, and the ``AliasedGroup`` for the apothem CLI.
|
|
4
|
+
|
|
5
|
+
Carries the cross-command building blocks the per-command modules consume:
|
|
6
|
+
the structured CLI-error type, the adapter protocol + load helpers, the
|
|
7
|
+
profile read/write helpers, the lifecycle-envelope builders, harness
|
|
8
|
+
selection, project-root resolution, and the drift/plan helpers. Extracted
|
|
9
|
+
verbatim from the former monolithic ``cli/__init__.py``; the patchable-helper
|
|
10
|
+
call sites inside selection/load helpers resolve through the ``apothem.cli``
|
|
11
|
+
package (``_pkg``) so the test patch seams keep landing."""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Protocol, cast
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
from click.shell_completion import CompletionItem
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
import apothem.cli as _pkg
|
|
26
|
+
from apothem.cli._json_formatter import emit_json
|
|
27
|
+
from apothem.harnesses._shared import install_driver
|
|
28
|
+
from apothem.harnesses._shared.install_driver import (
|
|
29
|
+
MaterializationError,
|
|
30
|
+
MaterializationResult,
|
|
31
|
+
MaterializationRun,
|
|
32
|
+
check_fidelity,
|
|
33
|
+
fidelity_is_faithful,
|
|
34
|
+
write_bytes_safely,
|
|
35
|
+
)
|
|
36
|
+
from apothem.lib.harness_registry import (
|
|
37
|
+
SUPPORTED_HARNESS_IDS,
|
|
38
|
+
HarnessRegistryEntry,
|
|
39
|
+
iter_harness_entries,
|
|
40
|
+
load_adapter_class,
|
|
41
|
+
)
|
|
42
|
+
from apothem.lib.profile import (
|
|
43
|
+
ProfileValidationError,
|
|
44
|
+
load_profile_file,
|
|
45
|
+
resolve_profile_path,
|
|
46
|
+
)
|
|
47
|
+
from apothem.schemas import profile_minimal_path
|
|
48
|
+
|
|
49
|
+
console = Console(highlight=False)
|
|
50
|
+
|
|
51
|
+
#: Shared Click context settings (``-h`` / ``--help`` aliases) for ``main`` and
|
|
52
|
+
#: the ``profile`` / ``harnesses`` sub-groups.
|
|
53
|
+
_CONTEXT = {"help_option_names": ["-h", "--help"]}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_ADAPTER_LOAD_ERRORS = (ImportError, AttributeError, TypeError, OSError)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_EXIT_EXPECTED = 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_EXIT_PARTIAL = 2
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _CliUserError(Exception):
|
|
66
|
+
"""Expected operator-facing CLI failure."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
code: str,
|
|
72
|
+
message: str,
|
|
73
|
+
field: str,
|
|
74
|
+
reason: str,
|
|
75
|
+
fix: str,
|
|
76
|
+
safe_value: object | None = None,
|
|
77
|
+
files_written: tuple[str, ...] = (),
|
|
78
|
+
) -> None:
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
self.code = code
|
|
81
|
+
self.message = message
|
|
82
|
+
self.field = field
|
|
83
|
+
self.reason = reason
|
|
84
|
+
self.fix = fix
|
|
85
|
+
self.safe_value = safe_value
|
|
86
|
+
self.files_written = files_written
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict[str, object]:
|
|
89
|
+
"""Return the machine-readable error object."""
|
|
90
|
+
return {
|
|
91
|
+
"code": self.code,
|
|
92
|
+
"message": self.message,
|
|
93
|
+
"field": self.field,
|
|
94
|
+
"reason": self.reason,
|
|
95
|
+
"fix": self.fix,
|
|
96
|
+
"safe_value": self.safe_value,
|
|
97
|
+
"files_written": list(self.files_written),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _Adapter(Protocol):
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str: ...
|
|
104
|
+
@property
|
|
105
|
+
def output_path(self) -> Path: ...
|
|
106
|
+
def install(self, profile: dict[str, Any]) -> object: ...
|
|
107
|
+
def update(self, profile: dict[str, Any]) -> object: ...
|
|
108
|
+
def uninstall(self) -> None: ...
|
|
109
|
+
def is_installed(self) -> bool: ...
|
|
110
|
+
def verify(self) -> bool: ...
|
|
111
|
+
|
|
112
|
+
# Optional project-scope extension (opt-in per adapter). Adapters
|
|
113
|
+
# that materialize into a project root rather than a user-scope
|
|
114
|
+
# configuration root declare ``requires_project = True`` and accept
|
|
115
|
+
# an optional ``project: Path | None`` keyword argument on their
|
|
116
|
+
# lifecycle methods. The CLI threads the operator-supplied
|
|
117
|
+
# ``--project <path>`` value through ``_materialize`` and routes it
|
|
118
|
+
# to the adapter only when the adapter's signature accepts it (via
|
|
119
|
+
# ``_invoke_with_project`` introspection).
|
|
120
|
+
requires_project: bool
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _adapter_requires_project(adapter: _Adapter) -> bool:
|
|
124
|
+
"""Return True iff *adapter* opts into the project-scope contract.
|
|
125
|
+
|
|
126
|
+
The opt-in is the literal boolean ``True`` on the ``requires_project``
|
|
127
|
+
attribute. Truthy non-boolean values (e.g., ``MagicMock`` instances
|
|
128
|
+
in unit-test fixtures, sentinel objects from incomplete adapter
|
|
129
|
+
stubs) do not satisfy the opt-in — explicit ratification is required.
|
|
130
|
+
"""
|
|
131
|
+
return getattr(adapter, "requires_project", False) is True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _adapter_resolve_output_path(adapter: _Adapter, project: Path | None) -> Path:
|
|
135
|
+
"""Resolve the adapter's output path, threading *project* when supported.
|
|
136
|
+
|
|
137
|
+
Project-scope adapters expose a ``resolve_output_path(project)``
|
|
138
|
+
method whose return is the concrete on-disk target under the
|
|
139
|
+
operator-supplied project root. Adapters without that method fall
|
|
140
|
+
back to the static ``output_path`` property — appropriate for
|
|
141
|
+
user-scope adapters whose target is determined by the harness's
|
|
142
|
+
home-rooted configuration directory.
|
|
143
|
+
"""
|
|
144
|
+
resolve_fn = getattr(adapter, "resolve_output_path", None)
|
|
145
|
+
if callable(resolve_fn):
|
|
146
|
+
return cast(Path, resolve_fn(project))
|
|
147
|
+
return adapter.output_path
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _stdin_is_interactive() -> bool:
|
|
151
|
+
"""Whether an interactive confirmation may be prompted (a TTY is attached).
|
|
152
|
+
|
|
153
|
+
A dedicated seam (rather than an inline ``sys.stdin.isatty()``) so the
|
|
154
|
+
blast-radius confirmation gate is exercisable under the test runner, where
|
|
155
|
+
stdin is a replayed stream rather than a terminal.
|
|
156
|
+
"""
|
|
157
|
+
return sys.stdin.isatty()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _path_is_within(target: Path, root: Path) -> bool:
|
|
161
|
+
"""Return True iff *target* resolves inside *root* (lexical containment)."""
|
|
162
|
+
try:
|
|
163
|
+
target.resolve(strict=False).relative_to(root.resolve(strict=False))
|
|
164
|
+
except ValueError:
|
|
165
|
+
return False
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _partition_blast_radius(
|
|
170
|
+
adapters: list[tuple[str, _Adapter]],
|
|
171
|
+
project_root: Path | None,
|
|
172
|
+
) -> tuple[list[tuple[str, Path]], list[tuple[str, Path]]]:
|
|
173
|
+
"""Split adapter targets into (under-project, outside-project/home-rooted).
|
|
174
|
+
|
|
175
|
+
Project-scope adapters materialize under the supplied ``--project`` root;
|
|
176
|
+
user-scope adapters materialize under the operator's real home directory,
|
|
177
|
+
i.e. outside that root. The partition is by concrete on-disk containment so
|
|
178
|
+
the disclosure stays truthful even if a home target happens to fall inside
|
|
179
|
+
an unusual project root.
|
|
180
|
+
"""
|
|
181
|
+
within: list[tuple[str, Path]] = []
|
|
182
|
+
outside: list[tuple[str, Path]] = []
|
|
183
|
+
for harness_id, adapter in adapters:
|
|
184
|
+
target = _adapter_resolve_output_path(adapter, project_root)
|
|
185
|
+
if project_root is not None and _path_is_within(target, project_root):
|
|
186
|
+
within.append((harness_id, target))
|
|
187
|
+
else:
|
|
188
|
+
outside.append((harness_id, target))
|
|
189
|
+
return within, outside
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _render_blast_radius(
|
|
193
|
+
con: Console,
|
|
194
|
+
within: list[tuple[str, Path]],
|
|
195
|
+
outside: list[tuple[str, Path]],
|
|
196
|
+
project_root: Path | None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Print the pre-write disclosure: targets grouped by project vs home root."""
|
|
199
|
+
total = len(within) + len(outside)
|
|
200
|
+
con.print(f"[bold]Apothem will write {total} configuration file(s):[/]")
|
|
201
|
+
width = max((len(hid) for hid, _ in [*within, *outside]), default=0)
|
|
202
|
+
if within:
|
|
203
|
+
con.print(f" [cyan]Under the project root[/] ({project_root}):")
|
|
204
|
+
for harness_id, target in within:
|
|
205
|
+
con.print(f" {harness_id.ljust(width)} -> {target}")
|
|
206
|
+
if outside:
|
|
207
|
+
con.print(" [yellow]Under your home directory[/] (outside --project):")
|
|
208
|
+
for harness_id, target in outside:
|
|
209
|
+
con.print(f" {harness_id.ljust(width)} -> {target}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_PLACEHOLDER_IDENTITY: dict[str, str] = {
|
|
213
|
+
"name": "Example User",
|
|
214
|
+
"email": "dev@example.invalid",
|
|
215
|
+
"website": "https://example.invalid",
|
|
216
|
+
"github": "example-user",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _placeholder_identity_fields(profile: dict[str, Any]) -> list[str]:
|
|
221
|
+
"""Return the identity fields still set to their shipped placeholder value."""
|
|
222
|
+
identity = profile.get("identity")
|
|
223
|
+
if not isinstance(identity, dict):
|
|
224
|
+
return []
|
|
225
|
+
return [
|
|
226
|
+
field
|
|
227
|
+
for field, placeholder in _PLACEHOLDER_IDENTITY.items()
|
|
228
|
+
if str(identity.get(field, "")).strip() == placeholder
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _placeholder_advisory_entry(
|
|
233
|
+
profile_path: Path, fields: list[str]
|
|
234
|
+
) -> dict[str, object]:
|
|
235
|
+
"""Build the lifecycle-envelope advisory for an unpersonalized identity."""
|
|
236
|
+
return {
|
|
237
|
+
"harness": None,
|
|
238
|
+
"outcome": "advisory",
|
|
239
|
+
"operation": "placeholder_identity",
|
|
240
|
+
"path": str(profile_path),
|
|
241
|
+
"fields": list(fields),
|
|
242
|
+
"message": (
|
|
243
|
+
"Profile identity is still the scaffold placeholder ("
|
|
244
|
+
+ ", ".join(fields)
|
|
245
|
+
+ "); personalize it so a real identity is projected. Edit the "
|
|
246
|
+
+ "profile or run 'apothem profile set identity.name \"Your Name\"'."
|
|
247
|
+
),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _invoke_with_project(
|
|
252
|
+
fn: Callable[..., object], *args: object, project: Path | None
|
|
253
|
+
) -> object:
|
|
254
|
+
"""Invoke *fn* passing ``project=`` only when its signature accepts it.
|
|
255
|
+
|
|
256
|
+
Backward-compatible bridge: existing user-scope adapters declare
|
|
257
|
+
``install(self, profile)`` / ``uninstall(self)`` and do not accept
|
|
258
|
+
a ``project`` parameter. Project-scope adapters declare
|
|
259
|
+
``install(self, profile, project=None)`` and similar. Introspection
|
|
260
|
+
via ``inspect.signature`` lets the CLI thread the operator-supplied
|
|
261
|
+
project root to the adapters that consume it without breaking the
|
|
262
|
+
older signatures.
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
sig = inspect.signature(fn)
|
|
266
|
+
except (TypeError, ValueError):
|
|
267
|
+
return fn(*args)
|
|
268
|
+
if "project" in sig.parameters:
|
|
269
|
+
return fn(*args, project=project)
|
|
270
|
+
return fn(*args)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _load_adapter(harness: str) -> _Adapter:
|
|
274
|
+
"""Load a harness adapter by name from the central registry."""
|
|
275
|
+
try:
|
|
276
|
+
entry = _pkg.get_harness_entry(harness)
|
|
277
|
+
except KeyError as exc:
|
|
278
|
+
available = ", ".join(SUPPORTED_HARNESS_IDS)
|
|
279
|
+
raise click.ClickException(
|
|
280
|
+
f"Unknown harness {harness!r}. Available harnesses: {available}."
|
|
281
|
+
) from exc
|
|
282
|
+
try:
|
|
283
|
+
return cast(_Adapter, load_adapter_class(entry)())
|
|
284
|
+
except _ADAPTER_LOAD_ERRORS as exc:
|
|
285
|
+
raise click.ClickException(
|
|
286
|
+
f"Failed to load harness {entry.public_id!r}: {exc}"
|
|
287
|
+
) from exc
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _all_adapters() -> list[_Adapter]:
|
|
291
|
+
"""Return instances of all registered harness adapters.
|
|
292
|
+
|
|
293
|
+
A broken adapter is surfaced as a yellow warning and skipped rather
|
|
294
|
+
than silently dropped — the sweep still returns the healthy adapters
|
|
295
|
+
but the operator sees which ones failed to load.
|
|
296
|
+
"""
|
|
297
|
+
adapters: list[_Adapter] = []
|
|
298
|
+
for entry in iter_harness_entries():
|
|
299
|
+
try:
|
|
300
|
+
adapters.append(cast(_Adapter, load_adapter_class(entry)()))
|
|
301
|
+
except _ADAPTER_LOAD_ERRORS as exc:
|
|
302
|
+
console.print(
|
|
303
|
+
f"[yellow]⚠[/] Failed to load adapter {entry.public_id!r}: {exc}",
|
|
304
|
+
style="yellow",
|
|
305
|
+
)
|
|
306
|
+
return adapters
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _resolve_profile_path(profile: str | None) -> Path:
|
|
310
|
+
"""Return the shared-profile path."""
|
|
311
|
+
return resolve_profile_path(profile)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _complete_harness(
|
|
315
|
+
ctx: click.Context, param: click.Parameter, incomplete: str
|
|
316
|
+
) -> list[CompletionItem]:
|
|
317
|
+
"""Shell-completion callback for the ``--harness`` option.
|
|
318
|
+
|
|
319
|
+
Offers the fifteen registered public harness ids plus the batch token
|
|
320
|
+
``all``, filtered to those beginning with the operator's partial input. The
|
|
321
|
+
candidate set is the static registry's public-id form (e.g. ``claude-code``),
|
|
322
|
+
so completion never drifts from the harnesses ``install``/``verify`` accept.
|
|
323
|
+
"""
|
|
324
|
+
candidates = [*SUPPORTED_HARNESS_IDS, "all"]
|
|
325
|
+
return [
|
|
326
|
+
CompletionItem(candidate)
|
|
327
|
+
for candidate in candidates
|
|
328
|
+
if candidate.startswith(incomplete)
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _load_profile(profile_path: Path) -> dict[str, Any]:
|
|
333
|
+
"""Load, validate, and normalize the shared profile YAML."""
|
|
334
|
+
return load_profile_file(profile_path).to_dict()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _write_profile_text_safely(
|
|
338
|
+
profile_path: Path,
|
|
339
|
+
text: str,
|
|
340
|
+
*,
|
|
341
|
+
operation: str,
|
|
342
|
+
) -> MaterializationResult:
|
|
343
|
+
"""Write a profile document through the shared atomic write boundary."""
|
|
344
|
+
result = write_bytes_safely(
|
|
345
|
+
profile_path,
|
|
346
|
+
text.encode("utf-8"),
|
|
347
|
+
install_root=profile_path.parent,
|
|
348
|
+
harness_name="profile",
|
|
349
|
+
operation=operation,
|
|
350
|
+
)
|
|
351
|
+
if result.outcome == "error":
|
|
352
|
+
raise _CliUserError(
|
|
353
|
+
code="profile.write_failed",
|
|
354
|
+
message="Apothem profile write failed.",
|
|
355
|
+
field="profile",
|
|
356
|
+
reason=result.message,
|
|
357
|
+
fix=(
|
|
358
|
+
"Choose a profile path inside a normal directory and check "
|
|
359
|
+
"file permissions."
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _profile_error(exc: ProfileValidationError) -> dict[str, object]:
|
|
366
|
+
"""Return the stable CLI error object for profile validation failures."""
|
|
367
|
+
return exc.diagnostic.to_dict()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _parse_set_value(raw: str) -> object:
|
|
371
|
+
"""Parse a ``profile set`` VALUE without YAML scalar-coercion footguns.
|
|
372
|
+
|
|
373
|
+
Explicit structured input (a value whose first non-space char is ``[`` or
|
|
374
|
+
``{``) is parsed as YAML so list/map nodes can be set. Explicit booleans
|
|
375
|
+
(``true``/``false``, case-insensitive) coerce to bool for the enforcement
|
|
376
|
+
flags. Every other scalar is preserved as a literal string — no
|
|
377
|
+
Norway-problem coercion of ``no``/``yes``/``on``/``off``, no date or number
|
|
378
|
+
coercion (``2024-01-01``, ``1.0`` stay strings). Schema validation, not
|
|
379
|
+
guesswork, decides whether the literal is acceptable.
|
|
380
|
+
"""
|
|
381
|
+
import yaml
|
|
382
|
+
|
|
383
|
+
stripped = raw.strip()
|
|
384
|
+
if stripped[:1] in {"[", "{"}:
|
|
385
|
+
try:
|
|
386
|
+
return yaml.safe_load(raw)
|
|
387
|
+
except yaml.YAMLError:
|
|
388
|
+
return raw
|
|
389
|
+
if stripped.lower() in {"true", "false"}:
|
|
390
|
+
return stripped.lower() == "true"
|
|
391
|
+
return raw
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _set_nested(data: dict[str, Any], dotted_key: str, value: object) -> dict[str, Any]:
|
|
395
|
+
"""Assign *value* at the dotted path *dotted_key* within *data*.
|
|
396
|
+
|
|
397
|
+
Walks the addressed path, creating intermediate maps only along it, and
|
|
398
|
+
sets the leaf. A single key with no dot keeps whole-node behavior. Sibling
|
|
399
|
+
keys and unaddressed structure are preserved. Mutates and returns *data*.
|
|
400
|
+
"""
|
|
401
|
+
parts = dotted_key.split(".")
|
|
402
|
+
node = data
|
|
403
|
+
for part in parts[:-1]:
|
|
404
|
+
existing = node.get(part)
|
|
405
|
+
if not isinstance(existing, dict):
|
|
406
|
+
existing = {}
|
|
407
|
+
node[part] = existing
|
|
408
|
+
node = existing
|
|
409
|
+
node[parts[-1]] = value
|
|
410
|
+
return data
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _format_error_plain(
|
|
414
|
+
error: dict[str, object], *, profile_path: Path | None = None
|
|
415
|
+
) -> str:
|
|
416
|
+
"""Format an expected error with the diagnostic fields operators need."""
|
|
417
|
+
files = error.get("files_written", [])
|
|
418
|
+
if isinstance(files, list) and files:
|
|
419
|
+
files_written = ", ".join(str(item) for item in files)
|
|
420
|
+
else:
|
|
421
|
+
files_written = "none"
|
|
422
|
+
lines = [str(error.get("message", "Apothem command failed."))]
|
|
423
|
+
if profile_path is not None:
|
|
424
|
+
lines.append(f"Profile: {profile_path}")
|
|
425
|
+
lines.extend(
|
|
426
|
+
[
|
|
427
|
+
f"Field: {error.get('field', 'command')}",
|
|
428
|
+
f"Reason: {error.get('reason', 'unknown failure')}",
|
|
429
|
+
f"Fix: {error.get('fix', 'Review the command input and retry.')}",
|
|
430
|
+
f"Files written: {files_written}.",
|
|
431
|
+
]
|
|
432
|
+
)
|
|
433
|
+
safe_value = error.get("safe_value")
|
|
434
|
+
if safe_value is not None:
|
|
435
|
+
lines.insert(-1, f"Safe value: {safe_value!r}")
|
|
436
|
+
return "\n".join(lines)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _error_envelope(
|
|
440
|
+
*,
|
|
441
|
+
command: str,
|
|
442
|
+
harness: str | None = None,
|
|
443
|
+
profile_path: Path | None = None,
|
|
444
|
+
project_root: Path | None = None,
|
|
445
|
+
error: dict[str, object],
|
|
446
|
+
files_written: list[str] | None = None,
|
|
447
|
+
results: list[dict[str, object]] | None = None,
|
|
448
|
+
warnings: list[dict[str, object]] | None = None,
|
|
449
|
+
status: str = "error",
|
|
450
|
+
) -> dict[str, object]:
|
|
451
|
+
"""Build the stable machine-readable lifecycle envelope."""
|
|
452
|
+
return {
|
|
453
|
+
"status": status,
|
|
454
|
+
"command": command,
|
|
455
|
+
"harness": harness,
|
|
456
|
+
"profile_path": str(profile_path) if profile_path is not None else None,
|
|
457
|
+
"project": str(project_root) if project_root is not None else None,
|
|
458
|
+
"files_written": files_written or [],
|
|
459
|
+
"results": results or [],
|
|
460
|
+
"warnings": warnings or [],
|
|
461
|
+
"error": error,
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _emit_expected_error(
|
|
466
|
+
*,
|
|
467
|
+
command: str,
|
|
468
|
+
fmt: str,
|
|
469
|
+
error: dict[str, object],
|
|
470
|
+
harness: str | None = None,
|
|
471
|
+
profile_path: Path | None = None,
|
|
472
|
+
project_root: Path | None = None,
|
|
473
|
+
files_written: list[str] | None = None,
|
|
474
|
+
results: list[dict[str, object]] | None = None,
|
|
475
|
+
warnings: list[dict[str, object]] | None = None,
|
|
476
|
+
exit_code: int = _EXIT_EXPECTED,
|
|
477
|
+
) -> None:
|
|
478
|
+
"""Emit an expected failure as JSON or a Click-formatted plain error."""
|
|
479
|
+
status = "partial" if exit_code == _EXIT_PARTIAL else "error"
|
|
480
|
+
if fmt == "json":
|
|
481
|
+
emit_json(
|
|
482
|
+
_error_envelope(
|
|
483
|
+
command=command,
|
|
484
|
+
harness=harness,
|
|
485
|
+
profile_path=profile_path,
|
|
486
|
+
project_root=project_root,
|
|
487
|
+
error=error,
|
|
488
|
+
files_written=files_written,
|
|
489
|
+
results=results,
|
|
490
|
+
warnings=warnings,
|
|
491
|
+
status=status,
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
sys.exit(exit_code)
|
|
495
|
+
message = _format_error_plain(error, profile_path=profile_path)
|
|
496
|
+
if exit_code == _EXIT_EXPECTED:
|
|
497
|
+
raise click.ClickException(message)
|
|
498
|
+
# click.ClickException hard-codes exit 1; emit the same `Error:` prefix
|
|
499
|
+
# by hand and preserve a non-default (partial) exit code in plain mode,
|
|
500
|
+
# for parity with the JSON branch above — a partial outcome exits 2 in
|
|
501
|
+
# both formats, never 2 under --json and 1 in plain.
|
|
502
|
+
click.echo(f"Error: {message}", err=True)
|
|
503
|
+
sys.exit(exit_code)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _lifecycle_envelope(
|
|
507
|
+
*,
|
|
508
|
+
status: str,
|
|
509
|
+
command: str,
|
|
510
|
+
action: str,
|
|
511
|
+
harness: str | None,
|
|
512
|
+
profile_path: Path | None,
|
|
513
|
+
project_root: Path | None,
|
|
514
|
+
files_written: list[str],
|
|
515
|
+
results: list[dict[str, object]],
|
|
516
|
+
warnings: list[dict[str, object]],
|
|
517
|
+
output_path: Path | None = None,
|
|
518
|
+
materialization: MaterializationRun | None = None,
|
|
519
|
+
) -> dict[str, object]:
|
|
520
|
+
"""Build a stable success or dry-run envelope."""
|
|
521
|
+
payload: dict[str, object] = {
|
|
522
|
+
"status": status,
|
|
523
|
+
"command": command,
|
|
524
|
+
"action": action,
|
|
525
|
+
"harness": harness,
|
|
526
|
+
"profile_path": str(profile_path) if profile_path is not None else None,
|
|
527
|
+
"project": str(project_root) if project_root is not None else None,
|
|
528
|
+
"files_written": files_written,
|
|
529
|
+
"results": results,
|
|
530
|
+
"warnings": warnings,
|
|
531
|
+
"error": None,
|
|
532
|
+
}
|
|
533
|
+
if output_path is not None:
|
|
534
|
+
payload["output_path"] = str(output_path)
|
|
535
|
+
if materialization is not None:
|
|
536
|
+
payload["materialization"] = materialization.to_dict()
|
|
537
|
+
return payload
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _result_dicts(
|
|
541
|
+
harness: str, materialization: MaterializationRun | None
|
|
542
|
+
) -> list[dict[str, object]]:
|
|
543
|
+
"""Return materialization result dictionaries tagged by public harness id."""
|
|
544
|
+
if materialization is None:
|
|
545
|
+
return []
|
|
546
|
+
return [
|
|
547
|
+
{"harness": harness, **result.to_dict()} for result in materialization.results
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _warning_dicts(
|
|
552
|
+
harness: str, materialization: MaterializationRun | None
|
|
553
|
+
) -> list[dict[str, object]]:
|
|
554
|
+
"""Return warning result dictionaries tagged by public harness id."""
|
|
555
|
+
if materialization is None:
|
|
556
|
+
return []
|
|
557
|
+
return [
|
|
558
|
+
{"harness": harness, **warning.to_dict()}
|
|
559
|
+
for warning in materialization.warnings
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _adapter_failure_result(
|
|
564
|
+
harness_id: str,
|
|
565
|
+
operation: str,
|
|
566
|
+
output_path: Path | None,
|
|
567
|
+
exc: Exception,
|
|
568
|
+
) -> dict[str, object]:
|
|
569
|
+
"""Return a per-harness error result for an adapter that raised mid-batch.
|
|
570
|
+
|
|
571
|
+
Adapter lifecycle code is the untrusted edge: one adapter raising must degrade
|
|
572
|
+
that harness to a recorded error and let the batch continue, mirroring the
|
|
573
|
+
install/_materialize error model rather than aborting with a bare traceback.
|
|
574
|
+
"""
|
|
575
|
+
return {
|
|
576
|
+
"harness": harness_id,
|
|
577
|
+
"outcome": "error",
|
|
578
|
+
"operation": operation,
|
|
579
|
+
"path": str(output_path) if output_path is not None else None,
|
|
580
|
+
"message": f"{type(exc).__name__}: {exc}",
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _load_adapter_for_entry(entry: HarnessRegistryEntry) -> _Adapter:
|
|
585
|
+
"""Load an adapter for a registry entry with structured CLI errors."""
|
|
586
|
+
try:
|
|
587
|
+
return cast(_Adapter, load_adapter_class(entry)())
|
|
588
|
+
except _ADAPTER_LOAD_ERRORS as exc:
|
|
589
|
+
raise _CliUserError(
|
|
590
|
+
code="harness.load_failed",
|
|
591
|
+
message="Apothem adapter loading failed.",
|
|
592
|
+
field="harness",
|
|
593
|
+
reason=f"{entry.public_id}: {exc}",
|
|
594
|
+
fix="Reinstall Apothem or inspect the adapter package import.",
|
|
595
|
+
) from exc
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _unknown_harness_error(harness: str) -> _CliUserError:
|
|
599
|
+
"""Return a structured unknown-harness error."""
|
|
600
|
+
available = ", ".join(SUPPORTED_HARNESS_IDS)
|
|
601
|
+
return _CliUserError(
|
|
602
|
+
code="harness.unknown",
|
|
603
|
+
message="Apothem validation failed.",
|
|
604
|
+
field="harness",
|
|
605
|
+
reason=f"{harness!r} is not a supported harness.",
|
|
606
|
+
fix=f"Use one of: {available}, or use 'all' for batch lifecycle commands.",
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _selected_harness_ids(
|
|
611
|
+
harness: str, *, shared_profile: dict[str, Any] | None, apply_excludes: bool
|
|
612
|
+
) -> list[str]:
|
|
613
|
+
"""Resolve one harness or the deterministic registry-wide selection."""
|
|
614
|
+
requested = harness.strip().lower()
|
|
615
|
+
if requested == "all":
|
|
616
|
+
selected = list(SUPPORTED_HARNESS_IDS)
|
|
617
|
+
if apply_excludes and shared_profile is not None:
|
|
618
|
+
excluded = set(shared_profile.get("exclude_harnesses", []))
|
|
619
|
+
selected = [item for item in selected if item not in excluded]
|
|
620
|
+
if not selected:
|
|
621
|
+
raise _CliUserError(
|
|
622
|
+
code="harness.none_selected",
|
|
623
|
+
message="Apothem validation failed.",
|
|
624
|
+
field="harness",
|
|
625
|
+
reason="batch selection resolved to zero harnesses.",
|
|
626
|
+
fix="Remove at least one entry from exclude_harnesses or select a named harness.",
|
|
627
|
+
)
|
|
628
|
+
return selected
|
|
629
|
+
try:
|
|
630
|
+
return [_pkg.get_harness_entry(harness).public_id]
|
|
631
|
+
except KeyError as exc:
|
|
632
|
+
raise _unknown_harness_error(harness) from exc
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _require_project_for_selection(
|
|
636
|
+
harness_ids: list[str], project_root: Path | None
|
|
637
|
+
) -> None:
|
|
638
|
+
"""Reject selected project-scope harnesses when no project root exists."""
|
|
639
|
+
project_scoped = [
|
|
640
|
+
harness_id
|
|
641
|
+
for harness_id in harness_ids
|
|
642
|
+
if _pkg.get_harness_entry(harness_id).scope == "project"
|
|
643
|
+
]
|
|
644
|
+
if project_scoped and project_root is None:
|
|
645
|
+
joined = ", ".join(project_scoped)
|
|
646
|
+
raise _CliUserError(
|
|
647
|
+
code="project.required",
|
|
648
|
+
message="Apothem validation failed.",
|
|
649
|
+
field="project",
|
|
650
|
+
reason=f"project-scope harnesses require --project: {joined}.",
|
|
651
|
+
fix="Pass --project PATH or select only user-scope harnesses.",
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _profile_scaffold_text() -> str:
|
|
656
|
+
"""Return a schema-valid minimal profile scaffold."""
|
|
657
|
+
import yaml
|
|
658
|
+
|
|
659
|
+
data = yaml.safe_load(profile_minimal_path().read_text(encoding="utf-8"))
|
|
660
|
+
return yaml.safe_dump(data, sort_keys=False)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class AliasedGroup(click.Group):
|
|
664
|
+
"""Click group that resolves subcommands case-insensitively."""
|
|
665
|
+
|
|
666
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
667
|
+
cmd = super().get_command(ctx, cmd_name)
|
|
668
|
+
if cmd is not None:
|
|
669
|
+
return cmd
|
|
670
|
+
lower = cmd_name.lower()
|
|
671
|
+
matches = [n for n in self.list_commands(ctx) if n.lower() == lower]
|
|
672
|
+
if len(matches) == 1:
|
|
673
|
+
return super().get_command(ctx, matches[0])
|
|
674
|
+
if len(matches) > 1:
|
|
675
|
+
ctx.fail(f"Ambiguous command {cmd_name!r}: matches {sorted(matches)}")
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _resolve_project_root(project: str | None) -> Path | None:
|
|
680
|
+
"""Resolve operator-supplied ``--project`` value to an absolute path."""
|
|
681
|
+
if project is None:
|
|
682
|
+
return None
|
|
683
|
+
project_root = Path(project).expanduser().resolve(strict=False)
|
|
684
|
+
if not project_root.exists():
|
|
685
|
+
raise _CliUserError(
|
|
686
|
+
code="project.not_found",
|
|
687
|
+
message="Apothem validation failed.",
|
|
688
|
+
field="project",
|
|
689
|
+
reason=f"project root does not exist: {project_root}",
|
|
690
|
+
fix="Create the project directory or pass an existing --project PATH.",
|
|
691
|
+
)
|
|
692
|
+
if not project_root.is_dir():
|
|
693
|
+
raise _CliUserError(
|
|
694
|
+
code="project.invalid_type",
|
|
695
|
+
message="Apothem validation failed.",
|
|
696
|
+
field="project",
|
|
697
|
+
reason=f"project root is not a directory: {project_root}",
|
|
698
|
+
fix="Pass a directory path as --project.",
|
|
699
|
+
)
|
|
700
|
+
return project_root
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
_project_option = click.option(
|
|
704
|
+
"--project",
|
|
705
|
+
default=None,
|
|
706
|
+
metavar="PATH",
|
|
707
|
+
help=(
|
|
708
|
+
"Project root for project-scope harnesses (e.g., cursor). Required "
|
|
709
|
+
"by adapters that materialize into a project tree rather than a "
|
|
710
|
+
"user-scope configuration directory."
|
|
711
|
+
),
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
# Shared `--harness` decorator for the lifecycle commands whose option is
|
|
715
|
+
# byte-identical (install, uninstall, update, rollback, verify, and their
|
|
716
|
+
# continuous-form aliases): required, name-or-'all', shell-completed. The
|
|
717
|
+
# `diff` and `quickstart` variants differ (distinct help, and quickstart's
|
|
718
|
+
# `default="all"`) and declare their own `--harness` inline, so this is the
|
|
719
|
+
# single source of truth only for the identical form.
|
|
720
|
+
_harness_option = click.option(
|
|
721
|
+
"--harness",
|
|
722
|
+
required=True,
|
|
723
|
+
metavar="NAME",
|
|
724
|
+
help="Harness adapter name or 'all' for every supported harness.",
|
|
725
|
+
shell_complete=_complete_harness,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# Shared `--profile` decorator for every command that resolves the shared
|
|
729
|
+
# profile (install, update, diff, verify, status, profile show/init/set/edit,
|
|
730
|
+
# and the continuous-form aliases): an optional path to the shared profile
|
|
731
|
+
# YAML, defaulting to the host's standard location when omitted. The single
|
|
732
|
+
# source of truth so the flag surface stays uniform across the lifecycle.
|
|
733
|
+
_profile_option = click.option(
|
|
734
|
+
"--profile",
|
|
735
|
+
default=None,
|
|
736
|
+
metavar="PATH",
|
|
737
|
+
help="Path to shared profile YAML.",
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _require_project_if_needed(
|
|
742
|
+
adapter: _Adapter, harness: str, project_root: Path | None
|
|
743
|
+
) -> None:
|
|
744
|
+
"""Reject a project-scope adapter invoked without ``--project``.
|
|
745
|
+
|
|
746
|
+
Project-scope adapters (``requires_project = True``) materialize into
|
|
747
|
+
an operator-supplied project tree; every lifecycle command must be
|
|
748
|
+
told where that tree is. User-scope adapters are unaffected.
|
|
749
|
+
"""
|
|
750
|
+
if _adapter_requires_project(adapter) and project_root is None:
|
|
751
|
+
raise _CliUserError(
|
|
752
|
+
code="project.required",
|
|
753
|
+
message="Apothem validation failed.",
|
|
754
|
+
field="project",
|
|
755
|
+
reason=(f"{harness!r} materializes a project-scope configuration surface."),
|
|
756
|
+
fix="Pass --project PATH for this harness.",
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _harness_roots(
|
|
761
|
+
entry: HarnessRegistryEntry,
|
|
762
|
+
adapter: _Adapter,
|
|
763
|
+
project_root: Path | None,
|
|
764
|
+
) -> tuple[Path | None, Path | None]:
|
|
765
|
+
"""Resolve the (harness_root, project_root) pair the install driver consumes.
|
|
766
|
+
|
|
767
|
+
The shared ``install_driver`` seams (``run_install`` / ``check_fidelity``)
|
|
768
|
+
take a user-scope ``harness_root`` (the home-rooted configuration directory)
|
|
769
|
+
XOR a project-scope ``project_root``. Project-scope adapters thread the
|
|
770
|
+
operator-supplied ``--project`` value; user-scope adapters resolve their
|
|
771
|
+
home root as the parent of the adapter's static ``output_path``.
|
|
772
|
+
"""
|
|
773
|
+
if entry.scope == "project":
|
|
774
|
+
return None, project_root
|
|
775
|
+
return _adapter_resolve_output_path(adapter, project_root).parent, None
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _drift_state(
|
|
779
|
+
entry: HarnessRegistryEntry,
|
|
780
|
+
adapter: _Adapter,
|
|
781
|
+
installed: bool,
|
|
782
|
+
project_root: Path | None,
|
|
783
|
+
shared_profile: dict[str, Any],
|
|
784
|
+
) -> str:
|
|
785
|
+
"""Classify a harness's profile-fidelity drift for the ``status`` command.
|
|
786
|
+
|
|
787
|
+
``absent`` when the adapter reports no install; otherwise the profile
|
|
788
|
+
fidelity verdict — ``in-sync`` when every projected anchor carries the
|
|
789
|
+
freshly-projected profile body verbatim, else ``drift``. The check is
|
|
790
|
+
read-only: it re-projects the profile in memory and compares against the
|
|
791
|
+
on-disk anchors, mutating nothing.
|
|
792
|
+
"""
|
|
793
|
+
if not installed:
|
|
794
|
+
return "absent"
|
|
795
|
+
harness_root, scoped_project = _harness_roots(entry, adapter, project_root)
|
|
796
|
+
results = check_fidelity(
|
|
797
|
+
entry.package_key,
|
|
798
|
+
harness_root=harness_root,
|
|
799
|
+
project_root=scoped_project,
|
|
800
|
+
profile=shared_profile,
|
|
801
|
+
)
|
|
802
|
+
return "in-sync" if fidelity_is_faithful(results) else "drift"
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _dry_run_plan(
|
|
806
|
+
entry: HarnessRegistryEntry,
|
|
807
|
+
adapter: _Adapter,
|
|
808
|
+
project_root: Path | None,
|
|
809
|
+
shared_profile: dict[str, Any],
|
|
810
|
+
) -> MaterializationRun:
|
|
811
|
+
"""Return the no-write dry-run plan for one harness, diffs included.
|
|
812
|
+
|
|
813
|
+
Routes through the same ``install_driver.run_install(dry_run=True, profile=…)``
|
|
814
|
+
seam the install path uses, so the result carries the unified diff already
|
|
815
|
+
present in each operator-owned target's ``.detail`` — no re-derivation.
|
|
816
|
+
"""
|
|
817
|
+
harness_root, scoped_project = _harness_roots(entry, adapter, project_root)
|
|
818
|
+
return install_driver.run_install(
|
|
819
|
+
entry.package_key,
|
|
820
|
+
harness_root=harness_root,
|
|
821
|
+
project_root=scoped_project,
|
|
822
|
+
dry_run=True,
|
|
823
|
+
profile=shared_profile,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _materialization_error(
|
|
828
|
+
exc: MaterializationError, *, files_written: list[str]
|
|
829
|
+
) -> dict[str, object]:
|
|
830
|
+
"""Convert a materialization exception to the CLI diagnostic contract."""
|
|
831
|
+
first = exc.run.errors[0] if exc.run.errors else None
|
|
832
|
+
return _CliUserError(
|
|
833
|
+
code="materialization.failed",
|
|
834
|
+
message="Apothem materialization failed.",
|
|
835
|
+
field=first.operation if first is not None else "materialization",
|
|
836
|
+
reason=first.message if first is not None else str(exc),
|
|
837
|
+
fix="Review the target path, permissions, and harness support files before retrying.",
|
|
838
|
+
files_written=tuple(files_written),
|
|
839
|
+
).to_dict()
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _configure_stdio() -> None:
|
|
843
|
+
"""Force UTF-8 stdio on Windows so Rich output renders correctly.
|
|
844
|
+
|
|
845
|
+
Runs at CLI invocation only — never at import time — so it cannot
|
|
846
|
+
disturb a host process's captured streams. ``reconfigure`` mutates
|
|
847
|
+
the existing stream in place rather than replacing ``sys.stdout``,
|
|
848
|
+
which would orphan and later close a wrapping process's buffer
|
|
849
|
+
(e.g. pytest's capture buffer).
|
|
850
|
+
"""
|
|
851
|
+
if sys.platform != "win32":
|
|
852
|
+
return
|
|
853
|
+
for stream_name in ("stdout", "stderr"):
|
|
854
|
+
stream = getattr(sys, stream_name)
|
|
855
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
856
|
+
if reconfigure is not None:
|
|
857
|
+
reconfigure(encoding="utf-8", errors="replace")
|