@r1z4x/tezgah 1.0.0
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 +738 -0
- package/LICENSE +21 -0
- package/MANIFEST +753 -0
- package/README.de.md +169 -0
- package/README.es.md +168 -0
- package/README.fr.md +166 -0
- package/README.md +161 -0
- package/README.tr.md +163 -0
- package/agents/tezgah-reviewer.md +70 -0
- package/bin/codegen +337 -0
- package/bin/consult +797 -0
- package/bin/tezgah-adhd +50 -0
- package/bin/tezgah-agents +45 -0
- package/bin/tezgah-capture +66 -0
- package/bin/tezgah-context +96 -0
- package/bin/tezgah-decisions +213 -0
- package/bin/tezgah-design +953 -0
- package/bin/tezgah-docs +1199 -0
- package/bin/tezgah-doctor +497 -0
- package/bin/tezgah-dsh +37 -0
- package/bin/tezgah-dsh.cmd +70 -0
- package/bin/tezgah-gate +75 -0
- package/bin/tezgah-import-ai-research +439 -0
- package/bin/tezgah-index +32 -0
- package/bin/tezgah-lessons +148 -0
- package/bin/tezgah-mcp +322 -0
- package/bin/tezgah-migrate +324 -0
- package/bin/tezgah-pony +50 -0
- package/bin/tezgah-research +772 -0
- package/bin/tezgah-rollback +93 -0
- package/bin/tezgah-route +124 -0
- package/bin/tezgah-setup +5798 -0
- package/bin/tezgah-status +383 -0
- package/bin/tezgah-task +607 -0
- package/bin/tezgah-taste +423 -0
- package/bin/tezgah-triage +485 -0
- package/bin/tezgah.js +36 -0
- package/commands/adhd.md +21 -0
- package/commands/ponytail.md +21 -0
- package/docs/README.md +112 -0
- package/docs/architecture.md +226 -0
- package/docs/citations-baseline.json +59 -0
- package/docs/clarity-baseline.json +116 -0
- package/docs/contract.md +416 -0
- package/docs/environment.md +141 -0
- package/docs/evidence.md +831 -0
- package/docs/feature-audit.md +139 -0
- package/docs/gate.md +407 -0
- package/docs/glossary.md +201 -0
- package/docs/hosts.md +400 -0
- package/docs/index.json +471 -0
- package/docs/judge.md +193 -0
- package/docs/layers.md +189 -0
- package/docs/models.md +160 -0
- package/docs/operations.md +666 -0
- package/docs/research.md +809 -0
- package/docs/results/paired-outcome-2026-10.jsonl +254 -0
- package/docs/results/paired-outcome-2026-10.md +204 -0
- package/docs/skills.md +236 -0
- package/docs/status-line.md +315 -0
- package/docs/testing.md +287 -0
- package/docs/vision.md +117 -0
- package/hooks/hooks.json +33 -0
- package/hooks/projects-auto-init.py +98 -0
- package/hooks/projects-posttooluse.py +149 -0
- package/hooks/projects-pretooluse.py +40 -0
- package/hooks/projects-stop.py +60 -0
- package/hooks/tezgah_agents.py +817 -0
- package/hooks/tezgah_apps.py +266 -0
- package/hooks/tezgah_attest.py +255 -0
- package/hooks/tezgah_clarity.py +171 -0
- package/hooks/tezgah_components.py +140 -0
- package/hooks/tezgah_context.py +2953 -0
- package/hooks/tezgah_embed.py +765 -0
- package/hooks/tezgah_gate.py +2736 -0
- package/hooks/tezgah_guard.py +177 -0
- package/hooks/tezgah_index.py +124 -0
- package/hooks/tezgah_integrity.py +5141 -0
- package/hooks/tezgah_judge.py +487 -0
- package/hooks/tezgah_lang.py +230 -0
- package/hooks/tezgah_lessons.py +210 -0
- package/hooks/tezgah_models.py +855 -0
- package/hooks/tezgah_paths.py +1132 -0
- package/hooks/tezgah_policy.py +1039 -0
- package/hooks/tezgah_rank.py +77 -0
- package/hooks/tezgah_replay.py +1109 -0
- package/hooks/tezgah_research.py +5176 -0
- package/hooks/tezgah_shapes.py +305 -0
- package/hooks/tezgah_skill_pick.py +227 -0
- package/hooks/tezgah_snapshot.py +490 -0
- package/hooks/tezgah_stopspec.py +320 -0
- package/hooks/tezgah_task.py +458 -0
- package/hooks/tezgah_taste.py +201 -0
- package/hooks/tezgah_untrusted.py +112 -0
- package/hooks/tezgah_update.py +430 -0
- package/hosts/codex/hook.py +268 -0
- package/hosts/codex/hooks.json +26 -0
- package/hosts/cursor/hook.py +461 -0
- package/hosts/cursor/hooks.json +44 -0
- package/hosts/dsh/hooks.json +21 -0
- package/hosts/dsh/statusline/lib/client.js +138 -0
- package/hosts/dsh/statusline/lib/index.js +104 -0
- package/hosts/dsh/statusline/package.json +28 -0
- package/hosts/omp/hook.py +277 -0
- package/hosts/omp/tezgah-hook.ts.in +701 -0
- package/hosts/opencode/plugins/tezgah.js +2742 -0
- package/hosts/opencode/tui/tezgah-tui.tsx +151 -0
- package/output-styles/tezgah.md +142 -0
- package/package.json +32 -0
- package/packaging/build.sh +342 -0
- package/packaging/install.ps1 +166 -0
- package/packaging/install.sh +56 -0
- package/packaging/upgrade.sh +142 -0
- package/skills/ai-research/0-autoresearch-skill/SKILL.md +411 -0
- package/skills/ai-research/0-autoresearch-skill/references/agent-continuity.md +95 -0
- package/skills/ai-research/0-autoresearch-skill/references/progress-reporting.md +166 -0
- package/skills/ai-research/0-autoresearch-skill/references/skill-routing.md +219 -0
- package/skills/ai-research/0-autoresearch-skill/templates/findings.md +44 -0
- package/skills/ai-research/0-autoresearch-skill/templates/research-log.md +41 -0
- package/skills/ai-research/01-model-architecture/litgpt/SKILL.md +469 -0
- package/skills/ai-research/01-model-architecture/litgpt/references/custom-models.md +569 -0
- package/skills/ai-research/01-model-architecture/litgpt/references/distributed-training.md +452 -0
- package/skills/ai-research/01-model-architecture/litgpt/references/supported-models.md +337 -0
- package/skills/ai-research/01-model-architecture/litgpt/references/training-recipes.md +620 -0
- package/skills/ai-research/01-model-architecture/mamba/SKILL.md +260 -0
- package/skills/ai-research/01-model-architecture/mamba/references/architecture-details.md +207 -0
- package/skills/ai-research/01-model-architecture/mamba/references/benchmarks.md +256 -0
- package/skills/ai-research/01-model-architecture/mamba/references/training-guide.md +389 -0
- package/skills/ai-research/01-model-architecture/nanogpt/SKILL.md +290 -0
- package/skills/ai-research/01-model-architecture/nanogpt/references/architecture.md +383 -0
- package/skills/ai-research/01-model-architecture/nanogpt/references/data.md +477 -0
- package/skills/ai-research/01-model-architecture/nanogpt/references/training.md +565 -0
- package/skills/ai-research/01-model-architecture/rwkv/SKILL.md +260 -0
- package/skills/ai-research/01-model-architecture/rwkv/references/architecture-details.md +345 -0
- package/skills/ai-research/01-model-architecture/rwkv/references/rwkv7.md +387 -0
- package/skills/ai-research/01-model-architecture/rwkv/references/state-management.md +370 -0
- package/skills/ai-research/01-model-architecture/torchtitan/SKILL.md +358 -0
- package/skills/ai-research/01-model-architecture/torchtitan/references/checkpoint.md +182 -0
- package/skills/ai-research/01-model-architecture/torchtitan/references/custom-models.md +259 -0
- package/skills/ai-research/01-model-architecture/torchtitan/references/float8.md +134 -0
- package/skills/ai-research/01-model-architecture/torchtitan/references/fsdp.md +127 -0
- package/skills/ai-research/02-tokenization/huggingface-tokenizers/SKILL.md +516 -0
- package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/algorithms.md +654 -0
- package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/integration.md +638 -0
- package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/pipeline.md +724 -0
- package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/training.md +566 -0
- package/skills/ai-research/02-tokenization/sentencepiece/SKILL.md +235 -0
- package/skills/ai-research/02-tokenization/sentencepiece/references/algorithms.md +201 -0
- package/skills/ai-research/02-tokenization/sentencepiece/references/training.md +305 -0
- package/skills/ai-research/03-fine-tuning/axolotl/SKILL.md +158 -0
- package/skills/ai-research/03-fine-tuning/axolotl/references/api.md +5549 -0
- package/skills/ai-research/03-fine-tuning/axolotl/references/dataset-formats.md +1030 -0
- package/skills/ai-research/03-fine-tuning/axolotl/references/index.md +16 -0
- package/skills/ai-research/03-fine-tuning/axolotl/references/other.md +3564 -0
- package/skills/ai-research/03-fine-tuning/llama-factory/SKILL.md +80 -0
- package/skills/ai-research/03-fine-tuning/llama-factory/references/_images.md +24 -0
- package/skills/ai-research/03-fine-tuning/llama-factory/references/advanced.md +1056 -0
- package/skills/ai-research/03-fine-tuning/llama-factory/references/getting_started.md +350 -0
- package/skills/ai-research/03-fine-tuning/llama-factory/references/index.md +20 -0
- package/skills/ai-research/03-fine-tuning/llama-factory/references/other.md +32 -0
- package/skills/ai-research/03-fine-tuning/peft/SKILL.md +431 -0
- package/skills/ai-research/03-fine-tuning/peft/references/advanced-usage.md +515 -0
- package/skills/ai-research/03-fine-tuning/peft/references/troubleshooting.md +481 -0
- package/skills/ai-research/03-fine-tuning/unsloth/SKILL.md +80 -0
- package/skills/ai-research/03-fine-tuning/unsloth/references/index.md +8 -0
- package/skills/ai-research/03-fine-tuning/unsloth/references/llms.md +83 -0
- package/skills/ai-research/04-mechanistic-interpretability/nnsight/SKILL.md +436 -0
- package/skills/ai-research/04-mechanistic-interpretability/nnsight/references/README.md +79 -0
- package/skills/ai-research/04-mechanistic-interpretability/nnsight/references/api.md +345 -0
- package/skills/ai-research/04-mechanistic-interpretability/nnsight/references/tutorials.md +301 -0
- package/skills/ai-research/04-mechanistic-interpretability/pyvene/SKILL.md +473 -0
- package/skills/ai-research/04-mechanistic-interpretability/pyvene/references/README.md +74 -0
- package/skills/ai-research/04-mechanistic-interpretability/pyvene/references/api.md +384 -0
- package/skills/ai-research/04-mechanistic-interpretability/pyvene/references/tutorials.md +377 -0
- package/skills/ai-research/04-mechanistic-interpretability/saelens/SKILL.md +386 -0
- package/skills/ai-research/04-mechanistic-interpretability/saelens/references/README.md +71 -0
- package/skills/ai-research/04-mechanistic-interpretability/saelens/references/api.md +334 -0
- package/skills/ai-research/04-mechanistic-interpretability/saelens/references/tutorials.md +319 -0
- package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/SKILL.md +346 -0
- package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/references/README.md +55 -0
- package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/references/api.md +363 -0
- package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/references/tutorials.md +340 -0
- package/skills/ai-research/05-data-processing/nemo-curator/SKILL.md +383 -0
- package/skills/ai-research/05-data-processing/nemo-curator/references/deduplication.md +88 -0
- package/skills/ai-research/05-data-processing/nemo-curator/references/filtering.md +103 -0
- package/skills/ai-research/05-data-processing/ray-data/SKILL.md +326 -0
- package/skills/ai-research/05-data-processing/ray-data/references/integration.md +83 -0
- package/skills/ai-research/05-data-processing/ray-data/references/transformations.md +84 -0
- package/skills/ai-research/06-post-training/grpo-rl-training/README.md +98 -0
- package/skills/ai-research/06-post-training/grpo-rl-training/SKILL.md +572 -0
- package/skills/ai-research/06-post-training/miles/SKILL.md +315 -0
- package/skills/ai-research/06-post-training/miles/references/api-reference.md +142 -0
- package/skills/ai-research/06-post-training/miles/references/troubleshooting.md +353 -0
- package/skills/ai-research/06-post-training/openrlhf/SKILL.md +249 -0
- package/skills/ai-research/06-post-training/openrlhf/references/algorithm-comparison.md +405 -0
- package/skills/ai-research/06-post-training/openrlhf/references/custom-rewards.md +531 -0
- package/skills/ai-research/06-post-training/openrlhf/references/hybrid-engine.md +288 -0
- package/skills/ai-research/06-post-training/openrlhf/references/multi-node-training.md +455 -0
- package/skills/ai-research/06-post-training/simpo/SKILL.md +219 -0
- package/skills/ai-research/06-post-training/simpo/references/datasets.md +479 -0
- package/skills/ai-research/06-post-training/simpo/references/hyperparameters.md +453 -0
- package/skills/ai-research/06-post-training/simpo/references/loss-functions.md +351 -0
- package/skills/ai-research/06-post-training/slime/SKILL.md +464 -0
- package/skills/ai-research/06-post-training/slime/references/api-reference.md +393 -0
- package/skills/ai-research/06-post-training/slime/references/troubleshooting.md +387 -0
- package/skills/ai-research/06-post-training/torchforge/SKILL.md +433 -0
- package/skills/ai-research/06-post-training/torchforge/references/api-reference.md +328 -0
- package/skills/ai-research/06-post-training/torchforge/references/troubleshooting.md +410 -0
- package/skills/ai-research/06-post-training/trl-fine-tuning/SKILL.md +455 -0
- package/skills/ai-research/06-post-training/trl-fine-tuning/references/dpo-variants.md +228 -0
- package/skills/ai-research/06-post-training/trl-fine-tuning/references/online-rl.md +83 -0
- package/skills/ai-research/06-post-training/trl-fine-tuning/references/reward-modeling.md +123 -0
- package/skills/ai-research/06-post-training/trl-fine-tuning/references/sft-training.md +169 -0
- package/skills/ai-research/06-post-training/verl/SKILL.md +391 -0
- package/skills/ai-research/06-post-training/verl/references/api-reference.md +302 -0
- package/skills/ai-research/06-post-training/verl/references/troubleshooting.md +392 -0
- package/skills/ai-research/07-safety-alignment/constitutional-ai/SKILL.md +290 -0
- package/skills/ai-research/07-safety-alignment/llamaguard/SKILL.md +337 -0
- package/skills/ai-research/07-safety-alignment/nemo-guardrails/SKILL.md +297 -0
- package/skills/ai-research/07-safety-alignment/prompt-guard/SKILL.md +312 -0
- package/skills/ai-research/08-distributed-training/accelerate/SKILL.md +332 -0
- package/skills/ai-research/08-distributed-training/accelerate/references/custom-plugins.md +454 -0
- package/skills/ai-research/08-distributed-training/accelerate/references/megatron-integration.md +490 -0
- package/skills/ai-research/08-distributed-training/accelerate/references/performance.md +526 -0
- package/skills/ai-research/08-distributed-training/deepspeed/SKILL.md +141 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/08.md +18 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/09.md +174 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/2020.md +379 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/2023.md +280 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/assets.md +180 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/index.md +36 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/mii.md +119 -0
- package/skills/ai-research/08-distributed-training/deepspeed/references/other.md +1192 -0
- package/skills/ai-research/08-distributed-training/megatron-core/SKILL.md +366 -0
- package/skills/ai-research/08-distributed-training/megatron-core/references/benchmarks.md +250 -0
- package/skills/ai-research/08-distributed-training/megatron-core/references/parallelism-guide.md +405 -0
- package/skills/ai-research/08-distributed-training/megatron-core/references/production-examples.md +474 -0
- package/skills/ai-research/08-distributed-training/megatron-core/references/training-recipes.md +548 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/SKILL.md +231 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_dcp_async_recipe.md +14 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_dcp_overview.md +19 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_dcp_recipe.md +18 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_ddp_notes.md +10 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_device_mesh_tutorial.md +22 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_examples_fsdp2.md +15 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_fsdp1_api.md +9 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_fsdp2_tutorial.md +45 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_fully_shard_api.md +49 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_tp_tutorial.md +22 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/ray_train_fsdp2_example.md +13 -0
- package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/torchtitan_fsdp_notes.md +14 -0
- package/skills/ai-research/08-distributed-training/pytorch-lightning/SKILL.md +346 -0
- package/skills/ai-research/08-distributed-training/pytorch-lightning/references/callbacks.md +437 -0
- package/skills/ai-research/08-distributed-training/pytorch-lightning/references/distributed.md +491 -0
- package/skills/ai-research/08-distributed-training/pytorch-lightning/references/hyperparameter-tuning.md +557 -0
- package/skills/ai-research/08-distributed-training/ray-train/SKILL.md +406 -0
- package/skills/ai-research/08-distributed-training/ray-train/references/multi-node.md +629 -0
- package/skills/ai-research/09-infrastructure/lambda-labs/SKILL.md +545 -0
- package/skills/ai-research/09-infrastructure/lambda-labs/references/advanced-usage.md +612 -0
- package/skills/ai-research/09-infrastructure/lambda-labs/references/troubleshooting.md +531 -0
- package/skills/ai-research/09-infrastructure/modal/SKILL.md +341 -0
- package/skills/ai-research/09-infrastructure/modal/references/advanced-usage.md +504 -0
- package/skills/ai-research/09-infrastructure/modal/references/troubleshooting.md +495 -0
- package/skills/ai-research/09-infrastructure/skypilot/SKILL.md +509 -0
- package/skills/ai-research/09-infrastructure/skypilot/references/advanced-usage.md +492 -0
- package/skills/ai-research/09-infrastructure/skypilot/references/troubleshooting.md +571 -0
- package/skills/ai-research/10-optimization/awq/SKILL.md +310 -0
- package/skills/ai-research/10-optimization/awq/references/advanced-usage.md +325 -0
- package/skills/ai-research/10-optimization/awq/references/troubleshooting.md +345 -0
- package/skills/ai-research/10-optimization/bitsandbytes/SKILL.md +411 -0
- package/skills/ai-research/10-optimization/bitsandbytes/references/memory-optimization.md +522 -0
- package/skills/ai-research/10-optimization/bitsandbytes/references/qlora-training.md +522 -0
- package/skills/ai-research/10-optimization/bitsandbytes/references/quantization-formats.md +448 -0
- package/skills/ai-research/10-optimization/flash-attention/SKILL.md +367 -0
- package/skills/ai-research/10-optimization/flash-attention/references/benchmarks.md +216 -0
- package/skills/ai-research/10-optimization/flash-attention/references/transformers-integration.md +294 -0
- package/skills/ai-research/10-optimization/gguf/SKILL.md +427 -0
- package/skills/ai-research/10-optimization/gguf/references/advanced-usage.md +505 -0
- package/skills/ai-research/10-optimization/gguf/references/troubleshooting.md +443 -0
- package/skills/ai-research/10-optimization/gptq/SKILL.md +450 -0
- package/skills/ai-research/10-optimization/gptq/references/calibration.md +338 -0
- package/skills/ai-research/10-optimization/gptq/references/integration.md +130 -0
- package/skills/ai-research/10-optimization/gptq/references/troubleshooting.md +96 -0
- package/skills/ai-research/10-optimization/hqq/SKILL.md +445 -0
- package/skills/ai-research/10-optimization/hqq/references/advanced-usage.md +529 -0
- package/skills/ai-research/10-optimization/hqq/references/troubleshooting.md +504 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/SKILL.md +319 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/references/architecture.md +320 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/references/biomedical.md +498 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/references/domain-specific.md +626 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/references/experiment-loop.md +129 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/references/optimizers.md +330 -0
- package/skills/ai-research/10-optimization/ml-training-recipes/references/scaling-and-selection.md +428 -0
- package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/SKILL.md +405 -0
- package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/references/benchmarks.md +394 -0
- package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/references/custom-tasks.md +425 -0
- package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/references/issues.md +395 -0
- package/skills/ai-research/11-evaluation/lm-evaluation-harness/SKILL.md +490 -0
- package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/api-evaluation.md +491 -0
- package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/benchmark-guide.md +489 -0
- package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/custom-tasks.md +603 -0
- package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/distributed-eval.md +520 -0
- package/skills/ai-research/11-evaluation/nemo-evaluator/SKILL.md +494 -0
- package/skills/ai-research/11-evaluation/nemo-evaluator/references/adapter-system.md +341 -0
- package/skills/ai-research/11-evaluation/nemo-evaluator/references/configuration.md +448 -0
- package/skills/ai-research/11-evaluation/nemo-evaluator/references/custom-benchmarks.md +316 -0
- package/skills/ai-research/11-evaluation/nemo-evaluator/references/execution-backends.md +362 -0
- package/skills/ai-research/12-inference-serving/llama-cpp/SKILL.md +258 -0
- package/skills/ai-research/12-inference-serving/llama-cpp/references/optimization.md +90 -0
- package/skills/ai-research/12-inference-serving/llama-cpp/references/quantization.md +214 -0
- package/skills/ai-research/12-inference-serving/llama-cpp/references/server.md +126 -0
- package/skills/ai-research/12-inference-serving/sglang/SKILL.md +442 -0
- package/skills/ai-research/12-inference-serving/sglang/references/deployment.md +491 -0
- package/skills/ai-research/12-inference-serving/sglang/references/radix-attention.md +414 -0
- package/skills/ai-research/12-inference-serving/sglang/references/structured-generation.md +542 -0
- package/skills/ai-research/12-inference-serving/tensorrt-llm/SKILL.md +187 -0
- package/skills/ai-research/12-inference-serving/tensorrt-llm/references/multi-gpu.md +299 -0
- package/skills/ai-research/12-inference-serving/tensorrt-llm/references/optimization.md +243 -0
- package/skills/ai-research/12-inference-serving/tensorrt-llm/references/serving.md +471 -0
- package/skills/ai-research/12-inference-serving/vllm/SKILL.md +364 -0
- package/skills/ai-research/12-inference-serving/vllm/references/optimization.md +227 -0
- package/skills/ai-research/12-inference-serving/vllm/references/quantization.md +285 -0
- package/skills/ai-research/12-inference-serving/vllm/references/server-deployment.md +256 -0
- package/skills/ai-research/12-inference-serving/vllm/references/troubleshooting.md +448 -0
- package/skills/ai-research/13-mlops/mlflow/SKILL.md +704 -0
- package/skills/ai-research/13-mlops/mlflow/references/deployment.md +745 -0
- package/skills/ai-research/13-mlops/mlflow/references/model-registry.md +771 -0
- package/skills/ai-research/13-mlops/mlflow/references/tracking.md +681 -0
- package/skills/ai-research/13-mlops/swanlab/SKILL.md +406 -0
- package/skills/ai-research/13-mlops/swanlab/references/integrations.md +305 -0
- package/skills/ai-research/13-mlops/swanlab/references/visualization.md +234 -0
- package/skills/ai-research/13-mlops/tensorboard/SKILL.md +629 -0
- package/skills/ai-research/13-mlops/tensorboard/references/integrations.md +639 -0
- package/skills/ai-research/13-mlops/tensorboard/references/profiling.md +546 -0
- package/skills/ai-research/13-mlops/tensorboard/references/visualization.md +621 -0
- package/skills/ai-research/13-mlops/weights-and-biases/SKILL.md +590 -0
- package/skills/ai-research/13-mlops/weights-and-biases/references/artifacts.md +585 -0
- package/skills/ai-research/13-mlops/weights-and-biases/references/integrations.md +701 -0
- package/skills/ai-research/13-mlops/weights-and-biases/references/sweeps.md +848 -0
- package/skills/ai-research/14-agents/a-evolve/SKILL.md +383 -0
- package/skills/ai-research/14-agents/a-evolve/references/README.md +831 -0
- package/skills/ai-research/14-agents/a-evolve/references/api.md +594 -0
- package/skills/ai-research/14-agents/a-evolve/references/architecture.md +417 -0
- package/skills/ai-research/14-agents/a-evolve/references/design-patterns.md +462 -0
- package/skills/ai-research/14-agents/a-evolve/references/examples.md +352 -0
- package/skills/ai-research/14-agents/a-evolve/references/issues.md +591 -0
- package/skills/ai-research/14-agents/a-evolve/references/releases.md +112 -0
- package/skills/ai-research/14-agents/a-evolve/references/tutorials.md +819 -0
- package/skills/ai-research/14-agents/autogpt/SKILL.md +403 -0
- package/skills/ai-research/14-agents/autogpt/references/advanced-usage.md +536 -0
- package/skills/ai-research/14-agents/autogpt/references/troubleshooting.md +421 -0
- package/skills/ai-research/14-agents/crewai/SKILL.md +498 -0
- package/skills/ai-research/14-agents/crewai/references/flows.md +439 -0
- package/skills/ai-research/14-agents/crewai/references/tools.md +430 -0
- package/skills/ai-research/14-agents/crewai/references/troubleshooting.md +481 -0
- package/skills/ai-research/14-agents/langchain/SKILL.md +480 -0
- package/skills/ai-research/14-agents/langchain/references/agents.md +500 -0
- package/skills/ai-research/14-agents/langchain/references/integration.md +563 -0
- package/skills/ai-research/14-agents/langchain/references/rag.md +601 -0
- package/skills/ai-research/14-agents/llamaindex/SKILL.md +569 -0
- package/skills/ai-research/14-agents/llamaindex/references/agents.md +84 -0
- package/skills/ai-research/14-agents/llamaindex/references/data_connectors.md +109 -0
- package/skills/ai-research/14-agents/llamaindex/references/query_engines.md +407 -0
- package/skills/ai-research/15-rag/chroma/SKILL.md +406 -0
- package/skills/ai-research/15-rag/chroma/references/integration.md +39 -0
- package/skills/ai-research/15-rag/faiss/SKILL.md +221 -0
- package/skills/ai-research/15-rag/faiss/references/index_types.md +281 -0
- package/skills/ai-research/15-rag/pinecone/SKILL.md +358 -0
- package/skills/ai-research/15-rag/pinecone/references/deployment.md +182 -0
- package/skills/ai-research/15-rag/qdrant/SKILL.md +493 -0
- package/skills/ai-research/15-rag/qdrant/references/advanced-usage.md +649 -0
- package/skills/ai-research/15-rag/qdrant/references/troubleshooting.md +632 -0
- package/skills/ai-research/15-rag/sentence-transformers/SKILL.md +255 -0
- package/skills/ai-research/15-rag/sentence-transformers/references/models.md +124 -0
- package/skills/ai-research/16-prompt-engineering/dspy/SKILL.md +590 -0
- package/skills/ai-research/16-prompt-engineering/dspy/references/examples.md +664 -0
- package/skills/ai-research/16-prompt-engineering/dspy/references/modules.md +476 -0
- package/skills/ai-research/16-prompt-engineering/dspy/references/optimizers.md +567 -0
- package/skills/ai-research/16-prompt-engineering/guidance/SKILL.md +572 -0
- package/skills/ai-research/16-prompt-engineering/guidance/references/backends.md +555 -0
- package/skills/ai-research/16-prompt-engineering/guidance/references/constraints.md +675 -0
- package/skills/ai-research/16-prompt-engineering/guidance/references/examples.md +768 -0
- package/skills/ai-research/16-prompt-engineering/instructor/SKILL.md +740 -0
- package/skills/ai-research/16-prompt-engineering/instructor/references/examples.md +108 -0
- package/skills/ai-research/16-prompt-engineering/instructor/references/providers.md +71 -0
- package/skills/ai-research/16-prompt-engineering/instructor/references/validation.md +607 -0
- package/skills/ai-research/16-prompt-engineering/outlines/SKILL.md +652 -0
- package/skills/ai-research/16-prompt-engineering/outlines/references/backends.md +616 -0
- package/skills/ai-research/16-prompt-engineering/outlines/references/examples.md +774 -0
- package/skills/ai-research/16-prompt-engineering/outlines/references/json_generation.md +653 -0
- package/skills/ai-research/17-observability/langsmith/SKILL.md +422 -0
- package/skills/ai-research/17-observability/langsmith/references/advanced-usage.md +549 -0
- package/skills/ai-research/17-observability/langsmith/references/troubleshooting.md +538 -0
- package/skills/ai-research/17-observability/phoenix/SKILL.md +475 -0
- package/skills/ai-research/17-observability/phoenix/references/advanced-usage.md +620 -0
- package/skills/ai-research/17-observability/phoenix/references/troubleshooting.md +539 -0
- package/skills/ai-research/18-multimodal/audiocraft/SKILL.md +564 -0
- package/skills/ai-research/18-multimodal/audiocraft/references/advanced-usage.md +667 -0
- package/skills/ai-research/18-multimodal/audiocraft/references/troubleshooting.md +505 -0
- package/skills/ai-research/18-multimodal/blip-2/SKILL.md +564 -0
- package/skills/ai-research/18-multimodal/blip-2/references/advanced-usage.md +681 -0
- package/skills/ai-research/18-multimodal/blip-2/references/troubleshooting.md +527 -0
- package/skills/ai-research/18-multimodal/clip/SKILL.md +253 -0
- package/skills/ai-research/18-multimodal/clip/references/applications.md +208 -0
- package/skills/ai-research/18-multimodal/cosmos-policy/SKILL.md +402 -0
- package/skills/ai-research/18-multimodal/cosmos-policy/references/libero-commands.md +175 -0
- package/skills/ai-research/18-multimodal/cosmos-policy/references/robocasa-commands.md +147 -0
- package/skills/ai-research/18-multimodal/llava/SKILL.md +304 -0
- package/skills/ai-research/18-multimodal/llava/references/training.md +198 -0
- package/skills/ai-research/18-multimodal/openpi/SKILL.md +480 -0
- package/skills/ai-research/18-multimodal/openpi/references/checkpoints-and-env-map.md +86 -0
- package/skills/ai-research/18-multimodal/openpi/references/config-recipes.md +50 -0
- package/skills/ai-research/18-multimodal/openpi/references/pytorch-gotchas.md +49 -0
- package/skills/ai-research/18-multimodal/openpi/references/remote-client-pattern.md +48 -0
- package/skills/ai-research/18-multimodal/openpi/references/training-debugging.md +46 -0
- package/skills/ai-research/18-multimodal/openvla-oft/SKILL.md +441 -0
- package/skills/ai-research/18-multimodal/openvla-oft/references/aloha-workflow.md +141 -0
- package/skills/ai-research/18-multimodal/openvla-oft/references/config-troubleshooting.md +97 -0
- package/skills/ai-research/18-multimodal/openvla-oft/references/libero-workflow.md +115 -0
- package/skills/ai-research/18-multimodal/openvla-oft/references/paper-and-checkpoints.md +84 -0
- package/skills/ai-research/18-multimodal/segment-anything/SKILL.md +500 -0
- package/skills/ai-research/18-multimodal/segment-anything/references/advanced-usage.md +590 -0
- package/skills/ai-research/18-multimodal/segment-anything/references/troubleshooting.md +485 -0
- package/skills/ai-research/18-multimodal/stable-diffusion/SKILL.md +519 -0
- package/skills/ai-research/18-multimodal/stable-diffusion/references/advanced-usage.md +717 -0
- package/skills/ai-research/18-multimodal/stable-diffusion/references/troubleshooting.md +556 -0
- package/skills/ai-research/18-multimodal/whisper/SKILL.md +317 -0
- package/skills/ai-research/18-multimodal/whisper/references/languages.md +190 -0
- package/skills/ai-research/19-emerging-techniques/knowledge-distillation/SKILL.md +458 -0
- package/skills/ai-research/19-emerging-techniques/knowledge-distillation/references/minillm.md +335 -0
- package/skills/ai-research/19-emerging-techniques/long-context/SKILL.md +536 -0
- package/skills/ai-research/19-emerging-techniques/long-context/references/extension_methods.md +469 -0
- package/skills/ai-research/19-emerging-techniques/long-context/references/fine_tuning.md +612 -0
- package/skills/ai-research/19-emerging-techniques/long-context/references/rope.md +403 -0
- package/skills/ai-research/19-emerging-techniques/model-merging/SKILL.md +497 -0
- package/skills/ai-research/19-emerging-techniques/model-merging/references/coefficient-tuning.md +415 -0
- package/skills/ai-research/19-emerging-techniques/model-merging/references/evaluation.md +463 -0
- package/skills/ai-research/19-emerging-techniques/model-merging/references/examples.md +429 -0
- package/skills/ai-research/19-emerging-techniques/model-merging/references/methods.md +353 -0
- package/skills/ai-research/19-emerging-techniques/model-pruning/SKILL.md +495 -0
- package/skills/ai-research/19-emerging-techniques/model-pruning/references/wanda.md +348 -0
- package/skills/ai-research/19-emerging-techniques/moe-training/SKILL.md +526 -0
- package/skills/ai-research/19-emerging-techniques/moe-training/references/architectures.md +433 -0
- package/skills/ai-research/19-emerging-techniques/moe-training/references/inference.md +349 -0
- package/skills/ai-research/19-emerging-techniques/moe-training/references/training.md +426 -0
- package/skills/ai-research/19-emerging-techniques/speculative-decoding/SKILL.md +467 -0
- package/skills/ai-research/19-emerging-techniques/speculative-decoding/references/lookahead.md +310 -0
- package/skills/ai-research/19-emerging-techniques/speculative-decoding/references/medusa.md +351 -0
- package/skills/ai-research/20-ml-paper-writing/academic-plotting/SKILL.md +479 -0
- package/skills/ai-research/20-ml-paper-writing/academic-plotting/references/data-visualization.md +509 -0
- package/skills/ai-research/20-ml-paper-writing/academic-plotting/references/diagram-generation.md +395 -0
- package/skills/ai-research/20-ml-paper-writing/academic-plotting/references/style-guide.md +305 -0
- package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/SKILL.md +982 -0
- package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/checklists.md +348 -0
- package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/citation-workflow.md +563 -0
- package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/reviewer-guidelines.md +349 -0
- package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/sources.md +165 -0
- package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/writing-guide.md +477 -0
- package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/SKILL.md +298 -0
- package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/references/slide-templates.md +505 -0
- package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/SKILL.md +270 -0
- package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/checklist.md +279 -0
- package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/reviewer-guidelines.md +116 -0
- package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/section-blueprints.md +400 -0
- package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/systems-conferences.md +261 -0
- package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/writing-patterns.md +231 -0
- package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/SKILL.md +384 -0
- package/skills/ai-research/21-research-ideation/creative-thinking-for-research/SKILL.md +366 -0
- package/skills/ai-research/22-agent-native-research-artifact/compiler/SKILL.md +245 -0
- package/skills/ai-research/22-agent-native-research-artifact/compiler/references/ara-schema.md +439 -0
- package/skills/ai-research/22-agent-native-research-artifact/compiler/references/exploration-tree-spec.md +125 -0
- package/skills/ai-research/22-agent-native-research-artifact/compiler/references/validation-checklist.md +149 -0
- package/skills/ai-research/22-agent-native-research-artifact/research-manager/SKILL.md +324 -0
- package/skills/ai-research/22-agent-native-research-artifact/research-manager/references/event-taxonomy.md +124 -0
- package/skills/ai-research/22-agent-native-research-artifact/research-manager/references/provenance-tags.md +190 -0
- package/skills/ai-research/22-agent-native-research-artifact/research-manager/references/session-protocol.md +162 -0
- package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/SKILL.md +322 -0
- package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/references/review-dimensions.md +182 -0
- package/skills/ai-research/SKILL.md +101 -0
- package/skills/ai-research/SOURCE +38 -0
- package/skills/ai-research/index/1-frame.md +18 -0
- package/skills/ai-research/index/2-data.md +21 -0
- package/skills/ai-research/index/3-train.md +48 -0
- package/skills/ai-research/index/4-measure.md +25 -0
- package/skills/ai-research/index/5-run.md +23 -0
- package/skills/ai-research/index/6-write.md +35 -0
- package/skills/ai-research/library.json +3456 -0
- package/skills/analyze-app/EVALS.md +82 -0
- package/skills/analyze-app/SKILL.md +188 -0
- package/skills/design-contract/EVALS.md +80 -0
- package/skills/design-contract/SKILL.md +171 -0
- package/skills/design-library/EVALS.md +85 -0
- package/skills/design-library/INDEX.md +59 -0
- package/skills/design-library/SKILL.md +58 -0
- package/skills/design-library/SOURCE +131 -0
- package/skills/design-library/adaptive-interfaces/skills/colour-independence/SKILL.md +90 -0
- package/skills/design-library/adaptive-interfaces/skills/flexible-typography/SKILL.md +83 -0
- package/skills/design-library/adaptive-interfaces/skills/information-density/SKILL.md +87 -0
- package/skills/design-library/adaptive-interfaces/skills/preference-audit/SKILL.md +70 -0
- package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/SKILL.md +86 -0
- package/skills/design-library/adaptive-interfaces/skills/responsive-review/SKILL.md +76 -0
- package/skills/design-library/adaptive-interfaces/skills/simplified-views/SKILL.md +97 -0
- package/skills/design-library/adaptive-interfaces/skills/specify/SKILL.md +63 -0
- package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/SKILL.md +100 -0
- package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/SKILL.md +61 -0
- package/skills/design-library/cognitive-accessibility/skills/assess-load/SKILL.md +48 -0
- package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/SKILL.md +60 -0
- package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/SKILL.md +61 -0
- package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/SKILL.md +71 -0
- package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/SKILL.md +67 -0
- package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/SKILL.md +60 -0
- package/skills/design-library/cognitive-accessibility/skills/plain-language-design/SKILL.md +78 -0
- package/skills/design-library/cognitive-accessibility/skills/review/SKILL.md +56 -0
- package/skills/design-library/cognitive-accessibility/skills/simplify/SKILL.md +39 -0
- package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/SKILL.md +64 -0
- package/skills/design-library/design-research/skills/behavioural-analytics/SKILL.md +33 -0
- package/skills/design-library/design-research/skills/qual-quant-triangulation/SKILL.md +33 -0
- package/skills/design-library/inclusive-interaction/skills/audit/SKILL.md +50 -0
- package/skills/design-library/inclusive-interaction/skills/design-flow/SKILL.md +58 -0
- package/skills/design-library/inclusive-interaction/skills/feedback-and-status/SKILL.md +76 -0
- package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/SKILL.md +70 -0
- package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/SKILL.md +99 -0
- package/skills/design-library/inclusive-interaction/skills/keyboard-review/SKILL.md +54 -0
- package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/SKILL.md +79 -0
- package/skills/design-library/inclusive-interaction/skills/multi-modal-input/SKILL.md +70 -0
- package/skills/design-library/inclusive-interaction/skills/touch-target-design/SKILL.md +71 -0
- package/skills/design-library/inclusive-interaction/skills/voice-interaction/SKILL.md +68 -0
- package/skills/design-library/visual-critique/skills/critique-affordance/SKILL.md +45 -0
- package/skills/design-library/visual-critique/skills/critique-brand-consistency/SKILL.md +42 -0
- package/skills/design-library/visual-critique/skills/critique-color/SKILL.md +44 -0
- package/skills/design-library/visual-critique/skills/critique-composition/SKILL.md +46 -0
- package/skills/design-library/visual-critique/skills/critique-information-density/SKILL.md +45 -0
- package/skills/design-library/visual-critique/skills/critique-typography/SKILL.md +45 -0
- package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/SKILL.md +40 -0
- package/skills/feature-audit/EVALS.md +83 -0
- package/skills/feature-audit/SKILL.md +326 -0
- package/skills/harness/EVALS.md +81 -0
- package/skills/harness/SKILL.md +88 -0
- package/skills/i-have-adhd/EVALS.md +80 -0
- package/skills/i-have-adhd/SKILL.md +183 -0
- package/skills/no-ai-slop/EVALS.md +89 -0
- package/skills/no-ai-slop/SKILL.md +209 -0
- package/skills/no-ai-slop/eval.md +52 -0
- package/skills/plan-add/EVALS.md +79 -0
- package/skills/plan-add/SKILL.md +179 -0
- package/skills/plan-add/render_table.py +72 -0
- package/skills/plan-status/EVALS.md +78 -0
- package/skills/plan-status/SKILL.md +52 -0
- package/skills/plan-sync/EVALS.md +80 -0
- package/skills/plan-sync/SKILL.md +69 -0
- package/skills/pm-frameworks/SKILL.md +33 -0
- package/skills/pm-frameworks/SOURCE +39 -0
- package/skills/pm-frameworks/intended-vs-implemented/SKILL.md +42 -0
- package/skills/pm-frameworks/opportunity-solution-tree/SKILL.md +64 -0
- package/skills/ponytail/EVALS.md +80 -0
- package/skills/ponytail/SKILL.md +124 -0
- package/skills/product-analysis/EVALS.md +103 -0
- package/skills/product-analysis/SKILL.md +361 -0
- package/skills/research/EVALS.md +104 -0
- package/skills/research/SKILL.md +570 -0
- package/skills/rl-env/SKILL.md +53 -0
- package/skills/rl-env/SOURCE +41 -0
- package/skills/rl-env/generate-nemo-gym-env/SKILL.md +169 -0
- package/skills/rl-env/generate-nemo-gym-env/references/architecture.md +114 -0
- package/skills/rl-env/generate-openenv-env/SKILL.md +183 -0
- package/skills/rl-env/generate-openenv-env/references/architecture.md +82 -0
- package/skills/rl-env/generate-ors-env/SKILL.md +209 -0
- package/skills/rl-env/generate-ors-env/references/architecture.md +113 -0
- package/skills/rl-env/generate-verifiers-env/SKILL.md +188 -0
- package/skills/rl-env/generate-verifiers-env/references/architecture.md +119 -0
- package/skills/rl-env/rl-env-from-description/SKILL.md +124 -0
- package/skills/rl-env/rl-env-from-description/references/interview.md +75 -0
- package/skills/rl-env/rl-env-from-description/references/nemo_gym.md +41 -0
- package/skills/rl-env/rl-env-from-description/references/openenv.md +35 -0
- package/skills/rl-env/rl-env-from-description/references/ors.md +50 -0
- package/skills/rl-env/rl-env-from-description/references/verifiers.md +39 -0
- package/skills/tezgah-contract/EVALS.md +78 -0
- package/skills/tezgah-contract/SKILL.md +792 -0
- package/statusline.py +129 -0
- package/workflows/graph-impact.js +153 -0
- package/workflows/graph-map.js +158 -0
- package/workflows/graph-review.js +170 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Where tezgah is armed, what it is armed with, and which kill switches are on.
|
|
3
|
+
|
|
4
|
+
Shared by every host adapter - the Claude Code hooks, the Codex hooks, the
|
|
5
|
+
Cursor hooks, the opencode plugin and the status line - so there is exactly one
|
|
6
|
+
answer to "is this repo armed". Stdlib only, importable from any Python.
|
|
7
|
+
|
|
8
|
+
Roots, in order of precedence:
|
|
9
|
+
1. TEZGAH_ROOTS - os.pathsep-separated list (CI and one-offs)
|
|
10
|
+
2. ~/.config/tezgah/config.json {"roots": ["~/Projects", "~/work"]}
|
|
11
|
+
3. ~/Projects - the historical default, so an existing setup keeps working
|
|
12
|
+
Hooks are spawned by the app, not by an interactive shell, so the config file
|
|
13
|
+
is the reliable channel; the env var is the escape hatch, not the contract.
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
HOME = os.path.expanduser("~")
|
|
20
|
+
CONFIG_DIR = os.path.join(
|
|
21
|
+
os.environ.get("XDG_CONFIG_HOME") or os.path.join(HOME, ".config"), "tezgah")
|
|
22
|
+
CONFIG = os.path.join(CONFIG_DIR, "config.json")
|
|
23
|
+
BIN_DIR = os.path.join(CONFIG_DIR, "bin")
|
|
24
|
+
STATE_DIR = os.path.join(CONFIG_DIR, "state")
|
|
25
|
+
CACHE = os.path.join(HOME, ".cache", "tezgah")
|
|
26
|
+
# Hosts that sandbox hook file writes (dsh workspace-write) deny writes to the
|
|
27
|
+
# global cache; state that must be written from a hook falls back to the
|
|
28
|
+
# platform temp dir, which the sandbox always allows. TEZGAH_FALLBACK_CACHE
|
|
29
|
+
# overrides the fallback (tests). Resolved by `fallback_cache()` rather than at
|
|
30
|
+
# import: `tempfile` costs ~4 ms of every gated call's import and the tool gate,
|
|
31
|
+
# which imports this module, never asks for a cache dir.
|
|
32
|
+
DEFAULT_ROOT = os.path.join(HOME, "Projects")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stable_root(path):
|
|
36
|
+
"""`path` as an install names it: one inside a Homebrew keg
|
|
37
|
+
`<brew>/Cellar/tezgah/<version>/libexec` is named through `<brew>/opt/tezgah`
|
|
38
|
+
when that link resolves to the keg, because the next `brew upgrade`'s
|
|
39
|
+
default cleanup deletes the keg. Anything else - a checkout, a release
|
|
40
|
+
prefix, npm, a keg opt/ does not name - is its own path. bin/tezgah-setup
|
|
41
|
+
renders with it and the hooks attest with it, so both read one root."""
|
|
42
|
+
parts = path.split(os.sep)
|
|
43
|
+
for i in range(1, len(parts) - 3):
|
|
44
|
+
if parts[i:i + 2] == ["Cellar", "tezgah"] and parts[i + 3] == "libexec":
|
|
45
|
+
base = os.sep.join(parts[:i]) or os.sep
|
|
46
|
+
opt = os.path.join(base, "opt", "tezgah")
|
|
47
|
+
if os.path.realpath(opt) != os.path.realpath(os.sep.join(parts[:i + 3])):
|
|
48
|
+
return path
|
|
49
|
+
return os.path.join(opt, *parts[i + 3:])
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# this file lives in <plugin>/hooks, so the plugin root is one level up and
|
|
54
|
+
# every path advertised to a model is derived from here rather than hardcoded
|
|
55
|
+
PLUGIN_ROOT = stable_root(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
56
|
+
CODEGRAPH = "codegraph"
|
|
57
|
+
ORX = "orx"
|
|
58
|
+
# Tools installed by their own installers land here without the user's shell PATH
|
|
59
|
+
# being updated (non-interactive hook/CI shells), so a lookup falls back to these
|
|
60
|
+
# per-user bin dirs before declaring a tool missing.
|
|
61
|
+
USER_BINS = (os.path.join(HOME, ".local", "bin"), os.path.join(HOME, ".cargo", "bin"),
|
|
62
|
+
# opencode's own installer puts its binary here
|
|
63
|
+
os.path.join(HOME, ".opencode", "bin"))
|
|
64
|
+
# canonical kill switches live in CONFIG_DIR; the pre-multi-host setup wrote
|
|
65
|
+
# them to ~/.claude, so that stays a recognized channel
|
|
66
|
+
OFF_DIRS = (CONFIG_DIR, os.path.join(HOME, ".claude"))
|
|
67
|
+
# The one list of every name `off()` and `armed()` are asked about, and of the
|
|
68
|
+
# repo marks read from a root: the installer's uninstall sweep
|
|
69
|
+
# (bin/tezgah-setup OFF_SWITCH_NAMES), the context's off-note (`switches()`),
|
|
70
|
+
# the opencode plugin's own `off()` and the gate's control rule all answer from
|
|
71
|
+
# it, and tests/test_paths.py fails when any of them names a switch it lacks.
|
|
72
|
+
# CORE_SWITCHES are the sixteen the always-on contract names. ADR 007 leaves
|
|
73
|
+
# UNCLASSIFIED_SWITCHES open - two `off()` names CORE does not list and two
|
|
74
|
+
# opt-in markers read with `armed()` - and until the owner classifies them the
|
|
75
|
+
# control rule protects them like the rest (bin/tezgah-docs carries the same
|
|
76
|
+
# set for its inventory check; the test pins the two equal).
|
|
77
|
+
CORE_SWITCHES = ("adhd-off", "consult-off", "docs-judge-off", "exec-mode.off",
|
|
78
|
+
"judge-off", "lang-off", "orchestrate-off", "ponytail-auto.off",
|
|
79
|
+
"pretooluse-off", "reminder-off", "research-off", "spec-off",
|
|
80
|
+
"task-off", "triage-off", "verify-off", "workspace-off")
|
|
81
|
+
UNCLASSIFIED_SWITCHES = ("agents-off", "skill-suggest-on", "taste-on",
|
|
82
|
+
"update-check-off")
|
|
83
|
+
SWITCHES = CORE_SWITCHES + UNCLASSIFIED_SWITCHES
|
|
84
|
+
REPO_MARKS = (".no-adhd", ".no-graph", ".no-lessons", ".no-plan-gate",
|
|
85
|
+
".no-ponytail", ".no-taste")
|
|
86
|
+
# Where each host keeps its config. One definition, shared by the installer
|
|
87
|
+
# (which writes into these) and the agent generator (which decides whose
|
|
88
|
+
# per-repo subagent files to render), so "is this host installed?" cannot mean
|
|
89
|
+
# two different things in the two places that ask.
|
|
90
|
+
XDG_CONFIG = os.environ.get("XDG_CONFIG_HOME") or os.path.join(HOME, ".config")
|
|
91
|
+
HOST_DIRS = {
|
|
92
|
+
"claude": os.path.join(HOME, ".claude"),
|
|
93
|
+
# codex reads a relocated home from CODEX_HOME (an embedding app such as Orca
|
|
94
|
+
# hands each account its own), and that home is the one a codex session
|
|
95
|
+
# actually loads - so it is the one to install into and to check. dsh below
|
|
96
|
+
# follows the same pattern for DSH_HOME.
|
|
97
|
+
"codex": os.environ.get("CODEX_HOME") or os.path.join(HOME, ".codex"),
|
|
98
|
+
"cursor": os.path.join(HOME, ".cursor"),
|
|
99
|
+
"opencode": os.path.join(XDG_CONFIG, "opencode"),
|
|
100
|
+
"dsh": os.environ.get("DSH_HOME") or os.path.join(HOME, ".dsh"),
|
|
101
|
+
"omp": os.path.join(HOME, ".omp"),
|
|
102
|
+
}
|
|
103
|
+
# a host can be installed with no config dir yet (a CLI on PATH is enough)
|
|
104
|
+
HOST_BINS = {"cursor": ("cursor-agent", "cursor"), "opencode": ("opencode",),
|
|
105
|
+
"dsh": ("dsh",), "omp": ("omp",)}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def host_installed(name):
|
|
109
|
+
"""True when this host is present on this machine.
|
|
110
|
+
|
|
111
|
+
The config dir is the primary signal; the CLI covers a host that is
|
|
112
|
+
installed but has not been run yet. Both the installer's detection report
|
|
113
|
+
and the per-repo agent generation answer with this."""
|
|
114
|
+
if os.path.isdir(HOST_DIRS.get(name) or ""):
|
|
115
|
+
return True
|
|
116
|
+
return any(which_user(b) for b in HOST_BINS.get(name, ()))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def config():
|
|
120
|
+
try:
|
|
121
|
+
with open(CONFIG, encoding="utf-8") as fh:
|
|
122
|
+
return json.load(fh)
|
|
123
|
+
except Exception: # missing, unreadable or malformed: fall back to defaults
|
|
124
|
+
return {}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def writable_dir(path):
|
|
128
|
+
"""True when `path` exists (or can be created) and a file can be written in it.
|
|
129
|
+
|
|
130
|
+
A sandboxed host denies this outside the workspace; callers use it to pick a
|
|
131
|
+
writable state dir instead of failing on the first write."""
|
|
132
|
+
try:
|
|
133
|
+
# The cache holds ledgers and pre-write snapshots of the user's files, so
|
|
134
|
+
# a dir tezgah creates is owner-only; audit SEC-05 measured 0755 from the
|
|
135
|
+
# default umask. makedirs applies the mode to the leaf it creates, and an
|
|
136
|
+
# existing dir keeps the mode the user gave it.
|
|
137
|
+
os.makedirs(path, mode=0o700, exist_ok=True)
|
|
138
|
+
# The probe name is unique per call (pid + random). A fixed name made two
|
|
139
|
+
# concurrent probes share one file: the sibling's remove deleted it under
|
|
140
|
+
# the first probe, whose remove then failed and read the dir as
|
|
141
|
+
# unwritable - which sent that session's whole ledger to the temp
|
|
142
|
+
# fallback. Unique names leave no shared state, so every OSError here is
|
|
143
|
+
# a real denial. (O_CREAT|O_EXCL would still need a unique name to tell
|
|
144
|
+
# "a sibling holds the probe" from "the dir is not writable"; with one,
|
|
145
|
+
# EEXIST cannot arise and no errno needs special-casing.)
|
|
146
|
+
probe = os.path.join(path, ".tezgah-write-probe-%d-%s"
|
|
147
|
+
% (os.getpid(), os.urandom(6).hex()))
|
|
148
|
+
with open(probe, "w", encoding="utf-8") as fh:
|
|
149
|
+
fh.write("")
|
|
150
|
+
os.remove(probe)
|
|
151
|
+
return True
|
|
152
|
+
except OSError:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# The dir cache_dir() resolved, and the pair of candidate paths it resolved.
|
|
157
|
+
# A session that starts resolved to the temp fallback and a later probe that
|
|
158
|
+
# succeeds must not move its state: the ledger, the session store and the nudge
|
|
159
|
+
# marks are all keyed on this answer, so a change mid-session splits one
|
|
160
|
+
# session's evidence across two files. Keyed on the candidates so a caller that
|
|
161
|
+
# repoints CACHE (tests) still gets a fresh answer.
|
|
162
|
+
_CHOSEN_FOR = None
|
|
163
|
+
_CHOSEN_DIR = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def fallback_cache():
|
|
167
|
+
"""The sandbox-writable fallback cache dir: `TEZGAH_FALLBACK_CACHE`, else
|
|
168
|
+
`<tempdir>/tezgah`.
|
|
169
|
+
|
|
170
|
+
Resolved on use rather than at import, because `tempfile` costs a few ms of
|
|
171
|
+
every gated call and the tool gate - which imports this module - never asks
|
|
172
|
+
for a cache dir. The env var is read here for the same reason: a hook process
|
|
173
|
+
has its environment fixed at start, so the answer cannot differ within one."""
|
|
174
|
+
override = os.environ.get("TEZGAH_FALLBACK_CACHE")
|
|
175
|
+
if override:
|
|
176
|
+
return override
|
|
177
|
+
import tempfile # deferred: see the note above
|
|
178
|
+
return os.path.join(tempfile.gettempdir(), "tezgah")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cache_dir():
|
|
182
|
+
"""A writable tezgah state dir: the global cache, else the temp fallback.
|
|
183
|
+
|
|
184
|
+
The global cache is preferred so state persists across sessions; sandboxed
|
|
185
|
+
hosts (dsh workspace-write) fall back to temp rather than dropping the
|
|
186
|
+
nudge/gate state on the floor.
|
|
187
|
+
|
|
188
|
+
Memoised: the answer is fixed for the life of the process once the candidates
|
|
189
|
+
are fixed, so every writer and reader in one session agrees on one dir. A
|
|
190
|
+
later probe that fails does not move a session's state to the other file."""
|
|
191
|
+
global _CHOSEN_FOR, _CHOSEN_DIR
|
|
192
|
+
candidates = (CACHE, fallback_cache())
|
|
193
|
+
if _CHOSEN_FOR == candidates:
|
|
194
|
+
return _CHOSEN_DIR
|
|
195
|
+
chosen = next((d for d in candidates if writable_dir(d)), CACHE)
|
|
196
|
+
# dir before key, so a concurrent caller reads either both old (it computes
|
|
197
|
+
# the same answer) or both new; never a stored-nowhere None
|
|
198
|
+
_CHOSEN_DIR, _CHOSEN_FOR = chosen, candidates
|
|
199
|
+
return chosen
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def ai_research_dir():
|
|
203
|
+
"""The vendored domain library, inside the plugin checkout that is running.
|
|
204
|
+
|
|
205
|
+
Derived from this file's location, like every other path tezgah advertises,
|
|
206
|
+
so it is right on Claude Code too, which runs the plugin from a copy."""
|
|
207
|
+
return os.path.join(PLUGIN_ROOT, "skills", "ai-research")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def roots():
|
|
211
|
+
"""Configured roots as absolute real paths, longest (most specific) first.
|
|
212
|
+
|
|
213
|
+
`"roots"` is a list; a string is taken as one root and anything else as the
|
|
214
|
+
default. A string iterated as a list armed `~` (all of HOME) and `.` (cwd)."""
|
|
215
|
+
env = os.environ.get("TEZGAH_ROOTS")
|
|
216
|
+
if env:
|
|
217
|
+
raw = env.split(os.pathsep)
|
|
218
|
+
else:
|
|
219
|
+
raw = config().get("roots")
|
|
220
|
+
raw = [raw] if isinstance(raw, str) else raw if isinstance(raw, list) else []
|
|
221
|
+
raw = [p for p in raw if isinstance(p, str) and p.strip()] or [DEFAULT_ROOT]
|
|
222
|
+
out = set()
|
|
223
|
+
for p in raw:
|
|
224
|
+
p = os.path.expanduser((p or "").strip())
|
|
225
|
+
if not p:
|
|
226
|
+
continue
|
|
227
|
+
try:
|
|
228
|
+
out.add(os.path.realpath(p))
|
|
229
|
+
except OSError:
|
|
230
|
+
pass
|
|
231
|
+
return sorted(out, key=len, reverse=True)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def root_for(path):
|
|
235
|
+
"""The configured root containing path, or None when tezgah is not armed here.
|
|
236
|
+
|
|
237
|
+
A linked worktree outside every root is armed when its main checkout is under
|
|
238
|
+
one, and answers its own top level: every caller treats the answer as a base
|
|
239
|
+
`path` sits under, so the configured root would not do there."""
|
|
240
|
+
try:
|
|
241
|
+
real = os.path.realpath(path)
|
|
242
|
+
except OSError:
|
|
243
|
+
return None
|
|
244
|
+
rs = roots()
|
|
245
|
+
for r in rs:
|
|
246
|
+
if real == r or real.startswith(r + os.sep):
|
|
247
|
+
return r
|
|
248
|
+
top = worktree_top(real)
|
|
249
|
+
if top:
|
|
250
|
+
main = _linked_main_at(top)
|
|
251
|
+
if main and any(main == r or main.startswith(r + os.sep) for r in rs):
|
|
252
|
+
return top
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _toplevel(real):
|
|
257
|
+
"""The nearest directory at or above `real` holding a `.git` entry, else None."""
|
|
258
|
+
cur = real
|
|
259
|
+
while not os.path.lexists(os.path.join(cur, ".git")):
|
|
260
|
+
parent = os.path.dirname(cur)
|
|
261
|
+
if parent == cur:
|
|
262
|
+
return None
|
|
263
|
+
cur = parent
|
|
264
|
+
return cur
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def linked_main(path):
|
|
268
|
+
"""The main checkout of the nearest linked worktree holding `path`, else None.
|
|
269
|
+
|
|
270
|
+
Nearest only: `worktrees` asks it about the repository it was handed, so a
|
|
271
|
+
repo nested inside a worktree (a vendored clone, a submodule) answers None and
|
|
272
|
+
never the checkout it sits in; `worktree_top` is the ancestor walk `root_for`
|
|
273
|
+
uses. Read from the `.git` pointer file (`gitdir: <main>/.git/worktrees/
|
|
274
|
+
<name>`), never from git: `root_for` runs on every gate call and a session
|
|
275
|
+
start's git forks are pinned. A plain checkout (`.git` is a directory), a
|
|
276
|
+
submodule (a `/modules/` pointer) and a bare main (no `.git` component)
|
|
277
|
+
answer None."""
|
|
278
|
+
top = _toplevel(os.path.realpath(path))
|
|
279
|
+
if not top:
|
|
280
|
+
return None
|
|
281
|
+
return _linked_main_at(top)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def worktree_top(path):
|
|
285
|
+
"""The top of the linked worktree holding `path`, else None.
|
|
286
|
+
|
|
287
|
+
Every `.git`-holding ancestor is tried, nearest first, so a path under the
|
|
288
|
+
worktree's own `.tezgah` or under a repo nested in it still answers the
|
|
289
|
+
worktree. A `.git` directory, a pointer git did not write and a submodule
|
|
290
|
+
pointer are all skipped in favour of an enclosing worktree."""
|
|
291
|
+
cur = os.path.realpath(path)
|
|
292
|
+
while True:
|
|
293
|
+
if os.path.lexists(os.path.join(cur, ".git")) and _linked_main_at(cur):
|
|
294
|
+
return cur
|
|
295
|
+
parent = os.path.dirname(cur)
|
|
296
|
+
if parent == cur:
|
|
297
|
+
return None
|
|
298
|
+
cur = parent
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _linked_main_at(top):
|
|
302
|
+
"""`linked_main` for one directory: its main checkout when `top` is a linked
|
|
303
|
+
worktree, else None. A `.git` pointer whose admin directory git did not write
|
|
304
|
+
(its `commondir` missing) is not a worktree."""
|
|
305
|
+
try:
|
|
306
|
+
with open(os.path.join(top, ".git"), encoding="utf-8") as fh:
|
|
307
|
+
line = fh.readline().strip()
|
|
308
|
+
except (OSError, UnicodeDecodeError): # a directory is the plain checkout
|
|
309
|
+
return None
|
|
310
|
+
if not line.startswith("gitdir:"):
|
|
311
|
+
return None
|
|
312
|
+
gitdir = os.path.normpath(os.path.join(top, line[len("gitdir:"):].strip()))
|
|
313
|
+
dotgit = os.path.dirname(os.path.dirname(gitdir))
|
|
314
|
+
if os.path.basename(os.path.dirname(gitdir)) != "worktrees" \
|
|
315
|
+
or os.path.basename(dotgit) != ".git" \
|
|
316
|
+
or not os.path.isfile(os.path.join(gitdir, "commondir")):
|
|
317
|
+
return None
|
|
318
|
+
return os.path.realpath(os.path.dirname(dotgit))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def worktrees(repo):
|
|
322
|
+
"""Every checkout of the repository `repo` is in, main first, as real paths;
|
|
323
|
+
[] outside a git checkout.
|
|
324
|
+
|
|
325
|
+
Fork-free, from `<main>/.git/worktrees/*/gitdir` (each names a linked
|
|
326
|
+
worktree's `.git`). An entry whose target is gone - deleted without `git
|
|
327
|
+
worktree remove` - is skipped: git itself keeps it, marked prunable, until
|
|
328
|
+
`git worktree prune`. Each checkout keeps its own `.tezgah`; this only names
|
|
329
|
+
them."""
|
|
330
|
+
main = linked_main(repo)
|
|
331
|
+
if not main:
|
|
332
|
+
main = _toplevel(os.path.realpath(repo))
|
|
333
|
+
if not main or not os.path.isdir(os.path.join(main, ".git")):
|
|
334
|
+
return []
|
|
335
|
+
base = os.path.join(main, ".git", "worktrees")
|
|
336
|
+
out = [main]
|
|
337
|
+
try:
|
|
338
|
+
names = sorted(os.listdir(base))
|
|
339
|
+
except OSError:
|
|
340
|
+
return out
|
|
341
|
+
for name in names:
|
|
342
|
+
try:
|
|
343
|
+
with open(os.path.join(base, name, "gitdir"), encoding="utf-8") as fh:
|
|
344
|
+
pointer = fh.readline().strip()
|
|
345
|
+
except (OSError, UnicodeDecodeError):
|
|
346
|
+
continue
|
|
347
|
+
dot = os.path.join(base, name, pointer)
|
|
348
|
+
if pointer and os.path.exists(dot):
|
|
349
|
+
out.append(os.path.realpath(os.path.dirname(dot)))
|
|
350
|
+
return out
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def which_user(name):
|
|
354
|
+
"""`name` on PATH, else in a known per-user bin dir, else None.
|
|
355
|
+
|
|
356
|
+
Accepts an absolute path (TEZGAH_*_BIN overrides) unchanged. Keeps detection
|
|
357
|
+
stable across shells: a tool installed to ~/.cargo/bin reads as present even
|
|
358
|
+
when a non-interactive shell never sourced the rc that adds it to PATH."""
|
|
359
|
+
import shutil # deferred: see the fallback_cache() note above
|
|
360
|
+
found = shutil.which(name)
|
|
361
|
+
if found or os.path.isabs(name):
|
|
362
|
+
return found
|
|
363
|
+
for d in USER_BINS:
|
|
364
|
+
candidate = os.path.join(d, name)
|
|
365
|
+
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
366
|
+
return candidate
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def python_cmd():
|
|
371
|
+
"""The interpreter a host hook should be started with.
|
|
372
|
+
|
|
373
|
+
Four channels, in the order a machine actually answers them: TEZGAH_PYTHON
|
|
374
|
+
(the override a user pins when their interpreter is not on PATH - Windows
|
|
375
|
+
ships `python`/`py` and never `python3`), this process's own absolute
|
|
376
|
+
interpreter (the one proven to work on this machine), then the names the
|
|
377
|
+
platforms ship, `py` last for Windows. The override is returned as given: an
|
|
378
|
+
absolute path outside PATH is a legitimate answer, and quietly starting a
|
|
379
|
+
different interpreter than the one pinned is worse than failing to start.
|
|
380
|
+
|
|
381
|
+
`which_user()` is the lookup, so a per-user bin dir counts and `shutil` stays
|
|
382
|
+
a deferred import - the tool gate imports this module on every call."""
|
|
383
|
+
override = os.environ.get("TEZGAH_PYTHON")
|
|
384
|
+
if override:
|
|
385
|
+
return override
|
|
386
|
+
# A real file only when this process was started from one: an embedded
|
|
387
|
+
# interpreter reports None or "", neither of which is a command.
|
|
388
|
+
if sys.executable and os.path.isabs(sys.executable) \
|
|
389
|
+
and os.path.isfile(sys.executable):
|
|
390
|
+
return sys.executable
|
|
391
|
+
for name in ("python3", "python", "py"):
|
|
392
|
+
found = which_user(name)
|
|
393
|
+
if found:
|
|
394
|
+
return found
|
|
395
|
+
# Nothing resolved anywhere: the first documented name, so the host reports
|
|
396
|
+
# "python3: not found" - a named error - instead of a command with no name.
|
|
397
|
+
return "python3"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def hook_command(script):
|
|
401
|
+
"""The command string a host hook manifest should carry for `script`.
|
|
402
|
+
|
|
403
|
+
One place formats the interpreter/script pair, so a manifest, a template and
|
|
404
|
+
an installer line cannot disagree about it. Both halves are quoted, because
|
|
405
|
+
either can carry a space (`C:\\Program Files\\Python\\python.exe`, a home dir
|
|
406
|
+
under `~/My Projects`) and the host runs this string through its own shell.
|
|
407
|
+
Plain quotes rather than `shlex.quote`: cmd.exe has no single-quote form, so
|
|
408
|
+
a POSIX-only quote would make the Windows half unreadable there."""
|
|
409
|
+
return '"%s" "%s"' % (python_cmd(), script)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def codegraph_bin():
|
|
413
|
+
"""The codegraph executable, or None when it is not installed.
|
|
414
|
+
|
|
415
|
+
Three channels: a pin (`TEZGAH_CODEGRAPH_BIN`), the config file, then PATH -
|
|
416
|
+
the same shape the retired codebase-memory-mcp resolver had, because the pin
|
|
417
|
+
is what a test or a CI job needs and the config is what an install writes."""
|
|
418
|
+
return which_user(os.environ.get("TEZGAH_CODEGRAPH_BIN")
|
|
419
|
+
or config().get("codegraph_bin") or CODEGRAPH)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# consult's HTTP providers: name -> (key env var, key file under HOME). codegen
|
|
423
|
+
# accepts the same three with `--provider`.
|
|
424
|
+
PROVIDER_KEYS = {
|
|
425
|
+
"openrouter": ("OPENROUTER_API_KEY", os.path.join(".config", "openrouter", "key")),
|
|
426
|
+
"deepseek": ("DEEPSEEK_API_KEY", os.path.join(".config", "deepseek", "key")),
|
|
427
|
+
"inception": ("INCEPTION_API_KEY", os.path.join(".config", "inception", "key")),
|
|
428
|
+
}
|
|
429
|
+
# The agent CLIs consult can ask non-interactively: name -> (argv before the
|
|
430
|
+
# model flag and the prompt, the model flag, the model family it answers with).
|
|
431
|
+
# Every flag was read from the CLI's own `--help` (omp 18.3.1, claude 2.1.283,
|
|
432
|
+
# codex-cli 0.153.4 `exec`, opencode 1.18.31 `run`, cursor-agent 2025.09.12);
|
|
433
|
+
# gemini is absent because no install was there to read. The prompt goes last
|
|
434
|
+
# as one argument and stdin is /dev/null, so a CLI that wants a login prints
|
|
435
|
+
# and exits instead of waiting for a key.
|
|
436
|
+
CONSULT_CLIS = {
|
|
437
|
+
"omp": (["omp", "-p", "--no-tools", "--no-session", "--no-rules",
|
|
438
|
+
"--no-skills", "--no-extensions", "--no-title"], "--model",
|
|
439
|
+
"whichever model omp is configured for"),
|
|
440
|
+
# `--tools` is variadic, so a boolean flag must follow it before the prompt
|
|
441
|
+
"claude": (["claude", "-p", "--tools", "", "--no-session-persistence"],
|
|
442
|
+
"--model", "Anthropic Claude"),
|
|
443
|
+
"codex": (["codex", "exec", "--skip-git-repo-check", "--ephemeral",
|
|
444
|
+
"-s", "read-only", "--color", "never"], "-m", "OpenAI GPT"),
|
|
445
|
+
"opencode": (["opencode", "run", "--pure"], "-m",
|
|
446
|
+
"whichever provider/model opencode is configured for"),
|
|
447
|
+
"cursor-agent": (["cursor-agent", "-p", "--output-format", "text"],
|
|
448
|
+
"--model", "whichever model Cursor is configured for"),
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def have_provider_key(provider=None):
|
|
453
|
+
"""True when an HTTP provider key is present (env var or key file): for
|
|
454
|
+
`provider`, else for any of them. codegen's question, and one half of
|
|
455
|
+
consult's."""
|
|
456
|
+
names = [provider] if provider else list(PROVIDER_KEYS)
|
|
457
|
+
return any(os.environ.get(PROVIDER_KEYS[p][0])
|
|
458
|
+
or os.path.exists(os.path.join(HOME, PROVIDER_KEYS[p][1]))
|
|
459
|
+
for p in names)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def consult_options():
|
|
463
|
+
"""The consult members this machine can run now: `cli:<name>` for each
|
|
464
|
+
agent CLI found, then each HTTP provider whose key is present. Empty means
|
|
465
|
+
the second opinion cannot run at all; every surface asks this.
|
|
466
|
+
|
|
467
|
+
TEZGAH_CONSULT_CLIS, when set, is the comma list of CLIs that may count
|
|
468
|
+
(empty: none) - the pin a test or CI job needs, as TEZGAH_CODEGRAPH_BIN is
|
|
469
|
+
for the graph, so the machine's own agent CLIs cannot answer for it."""
|
|
470
|
+
pin = os.environ.get("TEZGAH_CONSULT_CLIS")
|
|
471
|
+
allowed = CONSULT_CLIS if pin is None else pin.split(",")
|
|
472
|
+
return (["cli:" + n for n in CONSULT_CLIS if n in allowed and which_user(n)]
|
|
473
|
+
+ [p for p in PROVIDER_KEYS if have_provider_key(p)])
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def have_typesafe_key():
|
|
477
|
+
"""True when omp can resolve its TypeSafe (Jev) credential: the env var it
|
|
478
|
+
…
|
|
479
|
+
reads, or a record in its own login store (`omp auth login typesafe`). omp
|
|
480
|
+
spends this key on `judge()`, auto thinking, unexpected-stop and AI staging
|
|
481
|
+
and silently falls back to a chat model without it. `~/.config/typesafe/key`
|
|
482
|
+
is tezgah's own key-file convention, not a path omp opens, so a key file
|
|
483
|
+
alone does not count here - it only reaches omp through an export. The seam's
|
|
484
|
+
own question is `have_judge_key()`, and the two answers differ in both
|
|
485
|
+
directions."""
|
|
486
|
+
if os.environ.get("TYPESAFE_API_KEY"):
|
|
487
|
+
return True
|
|
488
|
+
store = os.path.join(HOME, ".omp", "agent", "agent.db")
|
|
489
|
+
import sqlite3 # deferred: ~4 ms of every gated call's import, for a store
|
|
490
|
+
try: # that only the health lines and this question ever open
|
|
491
|
+
with sqlite3.connect("file:%s?mode=ro" % store, uri=True) as conn:
|
|
492
|
+
return conn.execute(
|
|
493
|
+
"select 1 from auth_credentials where provider = ?",
|
|
494
|
+
("typesafe",)).fetchone() is not None
|
|
495
|
+
except (sqlite3.Error, OSError):
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def have_judge_key():
|
|
500
|
+
"""True when tezgah's own judgement seam resolves a credential: the env var
|
|
501
|
+
it reads, else its key file - and, when neither resolves, the OpenRouter
|
|
502
|
+
fallback's own two channels (`OPENROUTER_API_KEY`, `~/.config/openrouter/key`),
|
|
503
|
+
which are the channels `hooks/tezgah_judge.py`'s `credential()` picks
|
|
504
|
+
between. This is what `bin/tezgah-triage`, `bin/tezgah-docs` and the skill
|
|
505
|
+
picker spend. Never omp's login store: the seam does not open it.
|
|
506
|
+
|
|
507
|
+
Kept next to `have_typesafe_key()` because the two are asked together and a
|
|
508
|
+
report that showed one answer under both questions was wrong in both
|
|
509
|
+
directions - a key file alone (seam works, omp does not) and a login-store
|
|
510
|
+
record alone (omp works, seam does not). A blank file is not a credential
|
|
511
|
+
here either, so this stays in step with the seam's own `strip() or None`."""
|
|
512
|
+
for env, home in (("TYPESAFE_API_KEY", ".config/typesafe/key"),
|
|
513
|
+
("OPENROUTER_API_KEY", ".config/openrouter/key")):
|
|
514
|
+
if os.environ.get(env, "").strip():
|
|
515
|
+
return True
|
|
516
|
+
try:
|
|
517
|
+
with open(os.path.join(HOME, home), encoding="utf-8") as fh:
|
|
518
|
+
if fh.read().strip():
|
|
519
|
+
return True
|
|
520
|
+
except OSError:
|
|
521
|
+
pass
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def orx_bin():
|
|
526
|
+
"""The OpenResearch `orx` executable, or None when it is not installed.
|
|
527
|
+
|
|
528
|
+
TEZGAH_ORX_BIN points at a specific binary (tests, CI); otherwise the first
|
|
529
|
+
`orx` on PATH wins, then a known per-user bin dir. Mirrors `codegraph_bin()` so a
|
|
530
|
+
missing tool is a clean None, not a failed lookup at call time."""
|
|
531
|
+
return which_user(os.environ.get("TEZGAH_ORX_BIN") or ORX)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def omp_bin():
|
|
535
|
+
"""The omp CLI, or None when it is not installed.
|
|
536
|
+
|
|
537
|
+
TEZGAH_OMP_BIN points at a specific binary (tests, CI); otherwise the first
|
|
538
|
+
`omp` on PATH wins, then a known per-user bin dir - the lookup `orx_bin()`
|
|
539
|
+
makes. The installer registers its omp extension through `omp config`, so a
|
|
540
|
+
missing CLI has to be a clean None rather than a failed spawn."""
|
|
541
|
+
return which_user(os.environ.get("TEZGAH_OMP_BIN") or "omp")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def claude_bin():
|
|
545
|
+
"""The Claude Code CLI, or None when it is not installed.
|
|
546
|
+
|
|
547
|
+
TEZGAH_CLAUDE_BIN points at a specific binary (tests, CI); otherwise the
|
|
548
|
+
lookup `omp_bin()` makes. The installer registers tezgah's plugin through
|
|
549
|
+
`claude plugin`, so the suite must be able to keep the real CLI out."""
|
|
550
|
+
return which_user(os.environ.get("TEZGAH_CLAUDE_BIN") or "claude")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# The values `reply_lang` (config.json) takes. The value itself is the switch:
|
|
554
|
+
# `tr` (the default, and what an install without the key reads) asks for Turkish
|
|
555
|
+
# replies, `en` for English ones, and `any` drops the Stop language check.
|
|
556
|
+
REPLY_LANGS = ("tr", "en", "any")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def reply_lang():
|
|
560
|
+
"""The configured reply language, `tr` when unset or not one of REPLY_LANGS."""
|
|
561
|
+
cfg = config()
|
|
562
|
+
value = cfg.get("reply_lang") if isinstance(cfg, dict) else None
|
|
563
|
+
return value if value in REPLY_LANGS else "tr"
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# Switch latching (plan 051, ADR 018). A hook process that knows its session
|
|
567
|
+
# binds the session's ledger here (`tezgah_integrity.bind_session`), and from
|
|
568
|
+
# then on `off()` ignores a switch file whose ctime or mtime is newer than the
|
|
569
|
+
# ledger's first row: the agent being judged writes files too, and a switch it
|
|
570
|
+
# made mid-session would otherwise disarm the rule judging it. ctime is the
|
|
571
|
+
# point - `touch -t` backdates mtime, never ctime. Three ways through:
|
|
572
|
+
# - the user's own prompt named the switch (an `authorized` row, `note_turn`);
|
|
573
|
+
# - the file is `tezgah-setup --uninstall --full`'s stand-down (STAND_DOWN),
|
|
574
|
+
# which writes pretooluse-off mid-session on purpose;
|
|
575
|
+
# - the file is within the first row's second: the row's `ts` is whole seconds.
|
|
576
|
+
# An unbound process (every CLI, a hook with no session id) answers as before.
|
|
577
|
+
# ponytail: the first two are bytes the agent can also write (an
|
|
578
|
+
# `authorized` row, the stand-down text) - the control rule refuses the routes
|
|
579
|
+
# the gate sees, and an interpreter writing them is SECURITY.md's residual.
|
|
580
|
+
STAND_DOWN = "tezgah-setup --uninstall --full\n"
|
|
581
|
+
_LATCH = {"ledger": None, "since": None, "auth": None}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def latch(ledger):
|
|
585
|
+
"""Bind this process's `off()` to one session's ledger path (None unbinds)."""
|
|
586
|
+
_LATCH.update(ledger=ledger, since=None, auth=None)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _first_ts(ledger):
|
|
590
|
+
try:
|
|
591
|
+
with open(ledger, "rb") as fh:
|
|
592
|
+
ts = json.loads(fh.readline()).get("ts")
|
|
593
|
+
except (OSError, ValueError, AttributeError):
|
|
594
|
+
return None
|
|
595
|
+
return ts if isinstance(ts, (int, float)) else None
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _authorized(ledger):
|
|
599
|
+
"""The switch names this session's `authorized` rows carry, memoized on
|
|
600
|
+
the ledger's size: read only when a switch is newer than the first row."""
|
|
601
|
+
try:
|
|
602
|
+
size = os.path.getsize(ledger)
|
|
603
|
+
except OSError:
|
|
604
|
+
return frozenset()
|
|
605
|
+
memo = _LATCH["auth"]
|
|
606
|
+
if memo and memo[0] == size:
|
|
607
|
+
return memo[1]
|
|
608
|
+
names = set()
|
|
609
|
+
try:
|
|
610
|
+
with open(ledger, encoding="utf-8", errors="replace") as fh:
|
|
611
|
+
for line in fh:
|
|
612
|
+
if '"authorized"' not in line:
|
|
613
|
+
continue
|
|
614
|
+
try:
|
|
615
|
+
row = json.loads(line)
|
|
616
|
+
except ValueError:
|
|
617
|
+
continue
|
|
618
|
+
if row.get("kind") == "authorized" and isinstance(
|
|
619
|
+
row.get("authorized"), list):
|
|
620
|
+
names.update(n for n in row["authorized"] if isinstance(n, str))
|
|
621
|
+
except OSError:
|
|
622
|
+
return frozenset()
|
|
623
|
+
_LATCH["auth"] = (size, frozenset(names))
|
|
624
|
+
return _LATCH["auth"][1]
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _honored(path, name):
|
|
628
|
+
try:
|
|
629
|
+
# the link's own times too: `stat` follows a link, and a link made now
|
|
630
|
+
# to an old file would read as old
|
|
631
|
+
stats = (os.stat(path), os.lstat(path))
|
|
632
|
+
except OSError:
|
|
633
|
+
return False
|
|
634
|
+
ledger = _LATCH["ledger"]
|
|
635
|
+
if not ledger:
|
|
636
|
+
return True
|
|
637
|
+
if _LATCH["since"] is None:
|
|
638
|
+
_LATCH["since"] = _first_ts(ledger)
|
|
639
|
+
since = _LATCH["since"]
|
|
640
|
+
if since is None or max(t for st in stats for t in (st.st_mtime, st.st_ctime)) \
|
|
641
|
+
< since + 1:
|
|
642
|
+
return True
|
|
643
|
+
if name == "pretooluse-off":
|
|
644
|
+
try:
|
|
645
|
+
with open(path, encoding="utf-8") as fh:
|
|
646
|
+
if fh.read(len(STAND_DOWN) + 1) == STAND_DOWN:
|
|
647
|
+
return True
|
|
648
|
+
except (OSError, UnicodeDecodeError):
|
|
649
|
+
pass
|
|
650
|
+
return name in _authorized(ledger)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def off(name):
|
|
654
|
+
"""A kill switch, canonical (~/.config/tezgah) or legacy (~/.claude),
|
|
655
|
+
latched to the bound session (see `latch`)."""
|
|
656
|
+
return any(_honored(os.path.join(d, name), name) for d in OFF_DIRS)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def armed(name):
|
|
660
|
+
"""An opt-in marker, in the same places the kill switches live.
|
|
661
|
+
|
|
662
|
+
`off()` cannot express "the user asked for this": a capability that is off
|
|
663
|
+
until it is wanted needs the opposite file, or a rule the session was simply
|
|
664
|
+
never armed with and a rule the user switched off would look the same."""
|
|
665
|
+
return any(os.path.exists(os.path.join(d, name)) for d in OFF_DIRS)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
PONY_LEVEL = os.path.join(CONFIG_DIR, "ponytail.level")
|
|
669
|
+
PONY_LEVELS = ("lite", "full", "ultra")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def pony_level():
|
|
673
|
+
"""The armed ponytail intensity level, `full` unless the user set one.
|
|
674
|
+
|
|
675
|
+
One machine-wide setting rather than a per-session one: the switch is a
|
|
676
|
+
file `bin/tezgah-pony` writes, and a session-keyed level would need a
|
|
677
|
+
session id the CLI does not have. An unreadable or unknown value falls back
|
|
678
|
+
to the default instead of inventing a level."""
|
|
679
|
+
try:
|
|
680
|
+
with open(PONY_LEVEL, encoding="utf-8") as fh:
|
|
681
|
+
value = fh.read().strip().lower()
|
|
682
|
+
except OSError:
|
|
683
|
+
return "full"
|
|
684
|
+
return value if value in PONY_LEVELS else "full"
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def tool(name):
|
|
688
|
+
"""Stable installed path for a tezgah CLI, else the plugin's own copy, so
|
|
689
|
+
the text injected into a session names something that actually exists."""
|
|
690
|
+
for p in (os.path.join(BIN_DIR, name), os.path.join(PLUGIN_ROOT, "bin", name)):
|
|
691
|
+
if os.path.exists(p):
|
|
692
|
+
return p
|
|
693
|
+
return os.path.join(BIN_DIR, name)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# --- the per-project workspace: <repo>/.tezgah, the only place tezgah writes
|
|
697
|
+
# project state. It is ignored by the project's own git and carries a private
|
|
698
|
+
# git repository of its own, so the evidence that needs history (a research
|
|
699
|
+
# protocol committed before its results, a plan moved to done/) has one without
|
|
700
|
+
# ever entering the project's history.
|
|
701
|
+
WORKSPACE = ".tezgah"
|
|
702
|
+
# git's own XDG config directory: `~/.config/git/config`, read as one of the
|
|
703
|
+
# identity channels by `seed_identity`.
|
|
704
|
+
CONFIG_DIR_GIT = os.environ.get("XDG_CONFIG_HOME") or os.path.join(HOME, ".config")
|
|
705
|
+
WORKSPACE_IGNORES = ("/.tezgah/", "/.codegraph/")
|
|
706
|
+
# The private repository commits as the identity it is configured with - the one
|
|
707
|
+
# `ensure_workspace` copies from the project that owns it - and never as a name
|
|
708
|
+
# tezgah invented: an author is a claim about who wrote the work, and a log that
|
|
709
|
+
# keeps a fabricated one keeps a lie. The signing agent is the only thing
|
|
710
|
+
# overridden here, because a workspace commit must not wait on one.
|
|
711
|
+
WS_IDENTITY = ("-c", "commit.gpgsign=false",)
|
|
712
|
+
# What `ensure_workspace` copies from the project into the workspace. Reading
|
|
713
|
+
# `git config --get` in the project already falls back to the global value, so a
|
|
714
|
+
# machine that sets either one gets the same identity in both repositories.
|
|
715
|
+
IDENTITY_KEYS = ("user.name", "user.email")
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def workspace(repo):
|
|
719
|
+
"""`<repo>/.tezgah`: every per-project file tezgah keeps lives under it."""
|
|
720
|
+
return os.path.join(repo, WORKSPACE)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# The workspace paths whose text is injected as a standing constraint: each is
|
|
724
|
+
# checked for a symlink, because a clone can carry one pointing into its own tree.
|
|
725
|
+
WORKSPACE_INJECTED = (".tezgah", os.path.join(".tezgah", "lessons.md"),
|
|
726
|
+
os.path.join(".tezgah", "plans"),
|
|
727
|
+
os.path.join(".tezgah", "plans", "open"))
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _git_dirs(gitdir):
|
|
731
|
+
"""The checkout's gitdir and, for a linked worktree, the common dir."""
|
|
732
|
+
dirs = [gitdir]
|
|
733
|
+
try:
|
|
734
|
+
with open(os.path.join(gitdir, "commondir"), encoding="utf-8") as fh:
|
|
735
|
+
dirs.append(os.path.join(gitdir, fh.readline().strip()))
|
|
736
|
+
except OSError:
|
|
737
|
+
pass
|
|
738
|
+
return dirs
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _index_files(gitdir):
|
|
742
|
+
"""The index plus every `sharedindex.*` (a split index, `core.splitIndex`,
|
|
743
|
+
keeps most entries there) in the checkout's gitdir and the common dir."""
|
|
744
|
+
import glob # deferred: the gate imports this module per call
|
|
745
|
+
return [os.path.join(gitdir, "index")] + sorted(
|
|
746
|
+
p for d in _git_dirs(gitdir)
|
|
747
|
+
for p in glob.glob(os.path.join(d, "sharedindex.*")))
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _object_width(gitdir):
|
|
751
|
+
"""The object id width the repository's config names: 32 under
|
|
752
|
+
`extensions.objectFormat = sha256`, 20 when a config was read without it,
|
|
753
|
+
None when no config could be read. The index header does not carry it, and
|
|
754
|
+
a guessed width can be satisfied by ground object ids (review U1)."""
|
|
755
|
+
read = False
|
|
756
|
+
for d in _git_dirs(gitdir):
|
|
757
|
+
try:
|
|
758
|
+
with open(os.path.join(d, "config"), encoding="utf-8",
|
|
759
|
+
errors="replace") as fh:
|
|
760
|
+
text = fh.read()
|
|
761
|
+
except OSError:
|
|
762
|
+
continue
|
|
763
|
+
read = True
|
|
764
|
+
import re # deferred: the gate imports this module per call
|
|
765
|
+
if re.search(r"(?im)^\s*objectformat\s*=\s*sha256\s*$", text):
|
|
766
|
+
return 32
|
|
767
|
+
return 20 if read else None
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _index_paths(data, width=None):
|
|
771
|
+
"""The entry paths of a v2/v3 index, or None when they cannot be told.
|
|
772
|
+
|
|
773
|
+
Only the entries are read: the extensions after them carry other names, and
|
|
774
|
+
the untracked cache (UNTR, `core.untrackedCache`) records an untracked
|
|
775
|
+
`.tezgah` there - a whole-file byte search read the user's own workspace as
|
|
776
|
+
tracked. Each entry is 40 bytes of stat fields, the object id (`width`: 20
|
|
777
|
+
for SHA-1, 32 for SHA-256, from the config), 2 bytes of flags, 2 more when
|
|
778
|
+
v3 sets the extended bit, then the path, NUL-padded to a multiple of 8. A
|
|
779
|
+
parse is accepted only when every entry's name length in its flags (below
|
|
780
|
+
0xFFF) matches the path read. With no width known both are tried, and an
|
|
781
|
+
index both accept is as unreadable as one neither does."""
|
|
782
|
+
version = int.from_bytes(data[4:8], "big")
|
|
783
|
+
count = int.from_bytes(data[8:12], "big")
|
|
784
|
+
accepted = []
|
|
785
|
+
for size in ((width,) if width else (20, 32)):
|
|
786
|
+
paths, off = [], 12
|
|
787
|
+
try:
|
|
788
|
+
for _ in range(count):
|
|
789
|
+
flags = int.from_bytes(data[off + 40 + size:off + 42 + size], "big")
|
|
790
|
+
start = off + 42 + size + (2 if version >= 3 and flags & 0x4000 else 0)
|
|
791
|
+
end = data.index(b"\0", start)
|
|
792
|
+
if (flags & 0xFFF) < 0xFFF and flags & 0xFFF != end - start:
|
|
793
|
+
raise ValueError
|
|
794
|
+
paths.append(data[start:end])
|
|
795
|
+
off += (end - off + 8) & ~7
|
|
796
|
+
if off > len(data):
|
|
797
|
+
raise ValueError
|
|
798
|
+
except ValueError:
|
|
799
|
+
continue
|
|
800
|
+
accepted.append(paths)
|
|
801
|
+
return accepted[0] if len(accepted) == 1 else None
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def workspace_from_repo(root):
|
|
805
|
+
"""True when `<root>/.tezgah` came with the repository rather than from
|
|
806
|
+
tezgah, or that cannot be ruled out: its lessons and plans are then data,
|
|
807
|
+
never standing constraints, and no private workspace is initialised in it.
|
|
808
|
+
|
|
809
|
+
`.tezgah/` is the user's private workspace: ignored by the project and kept
|
|
810
|
+
in its own repository (`ensure_workspace`). A cloned hostile repository must
|
|
811
|
+
not be able to place rule text into the hook channel framed as a standing
|
|
812
|
+
constraint (audit L-16, SEC-11). It came with the clone when the project's
|
|
813
|
+
own index holds `.tezgah` itself (a symlink or a submodule) or any path under
|
|
814
|
+
it (`_index_tracks_workspace`), or when `.tezgah`, `lessons.md`, `plans` or
|
|
815
|
+
`plans/open` is a symlink - a tracked `.tezgah -> notes` holds no `.tezgah/`
|
|
816
|
+
path at all (review F3). An index that cannot be told is True here, the side
|
|
817
|
+
of a notice (review F9). ponytail: a tree with no `.git` (an unpacked
|
|
818
|
+
tarball) carries no record of where `.tezgah` came from, so it answers False
|
|
819
|
+
and is injected - the ceiling of a provenance check that reads git."""
|
|
820
|
+
if any(os.path.islink(os.path.join(root, rel)) for rel in WORKSPACE_INJECTED):
|
|
821
|
+
return True
|
|
822
|
+
return _index_tracks_workspace(root) is not False
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def workspace_tracked(root):
|
|
826
|
+
"""True only on positive evidence that the project's own index tracks
|
|
827
|
+
`.tezgah` or a path under it - the reading enforcement needs.
|
|
828
|
+
|
|
829
|
+
The injection side reads every can't-tell as "repository-provided"; the task
|
|
830
|
+
rule must not, because a None there switches the user's own plan guard off
|
|
831
|
+
in silence for a symlinked workspace of their own, an unreadable index or a
|
|
832
|
+
v4 query that timed out (review R2). The gate resolves the task on every
|
|
833
|
+
gated call, so the answer is kept in the cache keyed on the index files'
|
|
834
|
+
mtime and size: an unchanged index costs one stat per file and one small
|
|
835
|
+
read, never a re-parse or a `git ls-files` fork."""
|
|
836
|
+
gitdir = _gitdir_of(root)
|
|
837
|
+
if not gitdir:
|
|
838
|
+
return False
|
|
839
|
+
files = [p for p in _index_files(gitdir) if os.path.exists(p)]
|
|
840
|
+
if not files:
|
|
841
|
+
return False
|
|
842
|
+
try:
|
|
843
|
+
key = [[p, os.stat(p).st_mtime_ns, os.stat(p).st_size] for p in files]
|
|
844
|
+
except OSError:
|
|
845
|
+
return False
|
|
846
|
+
store = os.path.join(cache_dir(), "workspace-index.json")
|
|
847
|
+
try:
|
|
848
|
+
with open(store, encoding="utf-8") as fh:
|
|
849
|
+
known = json.load(fh)
|
|
850
|
+
except (OSError, ValueError):
|
|
851
|
+
known = {}
|
|
852
|
+
hit = known.get(root) if isinstance(known, dict) else None
|
|
853
|
+
if isinstance(hit, list) and len(hit) == 2 and hit[0] == key:
|
|
854
|
+
return hit[1] is True
|
|
855
|
+
answer = _index_tracks_workspace(root) is True
|
|
856
|
+
known = known if isinstance(known, dict) else {}
|
|
857
|
+
known[root] = [key, answer]
|
|
858
|
+
try:
|
|
859
|
+
tmp = "%s.%d.tmp" % (store, os.getpid())
|
|
860
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
861
|
+
json.dump(known, fh)
|
|
862
|
+
os.replace(tmp, store)
|
|
863
|
+
except OSError:
|
|
864
|
+
pass
|
|
865
|
+
return answer
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _gitdir_of(root):
|
|
869
|
+
"""The gitdir `<root>/.git` names: the directory itself, a worktree's
|
|
870
|
+
`gitdir:` target, "" for no `.git`, None for a `.git` that cannot be read."""
|
|
871
|
+
dot = os.path.join(root, ".git")
|
|
872
|
+
if not os.path.lexists(dot):
|
|
873
|
+
return ""
|
|
874
|
+
if os.path.isdir(dot):
|
|
875
|
+
return dot
|
|
876
|
+
try:
|
|
877
|
+
with open(dot, encoding="utf-8", errors="replace") as fh:
|
|
878
|
+
line = fh.readline().strip()
|
|
879
|
+
except OSError:
|
|
880
|
+
return None
|
|
881
|
+
if not line.startswith("gitdir:"):
|
|
882
|
+
return None
|
|
883
|
+
return os.path.join(root, line[len("gitdir:"):].strip())
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _index_tracks_workspace(root):
|
|
887
|
+
"""True when the project's index holds `.tezgah` or a `.tezgah/` path, False
|
|
888
|
+
when it was read and holds neither (or there is no `.git` or no index),
|
|
889
|
+
None when it cannot be told.
|
|
890
|
+
|
|
891
|
+
Read from the index files, not from `git ls-files`, because the session-start
|
|
892
|
+
git forks are pinned (GitSpawnBudget): v2/v3 store every path whole, so the
|
|
893
|
+
entries are parsed (`_index_paths`); v4 prefix-compresses paths, so only
|
|
894
|
+
there one `git ls-files` runs. Can't-tell is an unreadable `.git` pointer or
|
|
895
|
+
index, a foreign header, entries no known width parses, and a v4 query that
|
|
896
|
+
fails or times out. Entry paths are compared case-insensitively on every
|
|
897
|
+
platform: on APFS and NTFS defaults a tracked `.TEZGAH/lessons.md` is the
|
|
898
|
+
file `open` reads as `.tezgah/lessons.md` (review N2); a repository that
|
|
899
|
+
really tracks a `.TEZGAH` of its own reads as tracked, the safe side."""
|
|
900
|
+
gitdir = _gitdir_of(root)
|
|
901
|
+
if gitdir is None:
|
|
902
|
+
return None
|
|
903
|
+
if not gitdir:
|
|
904
|
+
return False
|
|
905
|
+
files = _index_files(gitdir)
|
|
906
|
+
if not os.path.exists(files[0]):
|
|
907
|
+
return False
|
|
908
|
+
width = _object_width(gitdir)
|
|
909
|
+
for path in files:
|
|
910
|
+
try:
|
|
911
|
+
with open(path, "rb") as fh:
|
|
912
|
+
data = fh.read()
|
|
913
|
+
except OSError:
|
|
914
|
+
return None
|
|
915
|
+
if data[:4] != b"DIRC":
|
|
916
|
+
return None
|
|
917
|
+
if int.from_bytes(data[4:8], "big") >= 4:
|
|
918
|
+
import subprocess # deferred: the gate imports this module per call
|
|
919
|
+
try:
|
|
920
|
+
out = subprocess.run(
|
|
921
|
+
("git", "-C", root, "ls-files", "-z", "--", ":(icase).tezgah"),
|
|
922
|
+
capture_output=True, timeout=5)
|
|
923
|
+
except Exception:
|
|
924
|
+
return None
|
|
925
|
+
return None if out.returncode else bool(out.stdout)
|
|
926
|
+
paths = _index_paths(data, width)
|
|
927
|
+
if paths is None:
|
|
928
|
+
return None
|
|
929
|
+
if any(p.lower() == b".tezgah" or p.lower().startswith(b".tezgah/")
|
|
930
|
+
for p in paths):
|
|
931
|
+
return True
|
|
932
|
+
return False
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def ensure_workspace(repo):
|
|
936
|
+
"""`<repo>/.tezgah`, created, ignored by the project and holding its own git
|
|
937
|
+
repository; None when `repo` is not a git work tree or a step failed.
|
|
938
|
+
|
|
939
|
+
The ignore lines are appended to the project's `.gitignore` only when
|
|
940
|
+
missing, after every line the user wrote, and nothing is staged. A second
|
|
941
|
+
call costs three stats and one file read: the subprocess runs only when
|
|
942
|
+
`.tezgah/.git` does not exist yet. Fail-open: a read-only tree returns None
|
|
943
|
+
and the session goes on."""
|
|
944
|
+
if not os.path.exists(os.path.join(repo, ".git")):
|
|
945
|
+
return None
|
|
946
|
+
ws = workspace(repo)
|
|
947
|
+
# A `.tezgah` that is a symlink (or anything but a directory) came with the
|
|
948
|
+
# repository: following it would `git init` inside the target the clone
|
|
949
|
+
# chose and write the private workspace there (review F3, audit L-16).
|
|
950
|
+
if os.path.islink(ws) or (os.path.lexists(ws) and not os.path.isdir(ws)):
|
|
951
|
+
return None
|
|
952
|
+
# The same for a `.tezgah` the project tracks (any case: `.TEZGAH` is this
|
|
953
|
+
# directory on APFS/NTFS, review N2). Asked only before the first `git
|
|
954
|
+
# init`; once tezgah's own repository is there, the question was answered.
|
|
955
|
+
if (not os.path.exists(os.path.join(ws, ".git"))
|
|
956
|
+
and workspace_from_repo(repo)):
|
|
957
|
+
return None
|
|
958
|
+
try:
|
|
959
|
+
os.makedirs(ws, exist_ok=True)
|
|
960
|
+
path = os.path.join(repo, ".gitignore")
|
|
961
|
+
try:
|
|
962
|
+
with open(path, encoding="utf-8") as fh:
|
|
963
|
+
body = fh.read()
|
|
964
|
+
except FileNotFoundError:
|
|
965
|
+
body = ""
|
|
966
|
+
have = {line.strip().strip("/") for line in body.splitlines()}
|
|
967
|
+
missing = [line for line in WORKSPACE_IGNORES if line.strip("/") not in have]
|
|
968
|
+
if missing:
|
|
969
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
970
|
+
fh.write(("\n" if body and not body.endswith("\n") else "")
|
|
971
|
+
+ "".join(line + "\n" for line in missing))
|
|
972
|
+
if not os.path.exists(os.path.join(ws, ".git")):
|
|
973
|
+
import subprocess # deferred: the gate imports this module per call
|
|
974
|
+
if subprocess.run(["git", "init", "-q", ws],
|
|
975
|
+
capture_output=True).returncode:
|
|
976
|
+
return None
|
|
977
|
+
seed_identity(repo, ws)
|
|
978
|
+
except (OSError, ValueError):
|
|
979
|
+
return None
|
|
980
|
+
return ws
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _git_config_paths(repo):
|
|
984
|
+
"""The config files that could hold this project's identity, most local first.
|
|
985
|
+
|
|
986
|
+
`git config` would answer this in one fork, and a session start forks git for
|
|
987
|
+
its own questions - the budget test counts them per question - so the two
|
|
988
|
+
files are read here instead: the repository's own config (following a
|
|
989
|
+
worktree's `gitdir:` pointer, and its common config), then the user's."""
|
|
990
|
+
out = []
|
|
991
|
+
dot = os.path.join(repo, ".git")
|
|
992
|
+
if os.path.isfile(dot):
|
|
993
|
+
try:
|
|
994
|
+
with open(dot, encoding="utf-8", errors="replace") as fh:
|
|
995
|
+
line = fh.readline().strip()
|
|
996
|
+
except OSError:
|
|
997
|
+
line = ""
|
|
998
|
+
if line.startswith("gitdir:"):
|
|
999
|
+
gitdir = line[len("gitdir:"):].strip()
|
|
1000
|
+
gitdir = gitdir if os.path.isabs(gitdir) else os.path.join(repo, gitdir)
|
|
1001
|
+
out += [os.path.join(gitdir, "config"),
|
|
1002
|
+
os.path.join(gitdir, "..", "..", "config")]
|
|
1003
|
+
elif os.path.isdir(dot):
|
|
1004
|
+
out.append(os.path.join(dot, "config"))
|
|
1005
|
+
out.append(os.path.join(HOME, ".gitconfig"))
|
|
1006
|
+
out.append(os.path.join(CONFIG_DIR_GIT, "git", "config"))
|
|
1007
|
+
return out
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def _identity_in(path):
|
|
1011
|
+
"""`(name, email)` from one git config file. No `include` is followed and no
|
|
1012
|
+
value is expanded: the plain `[user]` lines are what a project sets."""
|
|
1013
|
+
name = email = None
|
|
1014
|
+
section = ""
|
|
1015
|
+
try:
|
|
1016
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
1017
|
+
for line in fh:
|
|
1018
|
+
line = line.strip()
|
|
1019
|
+
if not line or line[0] in "#;":
|
|
1020
|
+
continue
|
|
1021
|
+
if line.startswith("[") and line.endswith("]"):
|
|
1022
|
+
section = line[1:-1].split('"')[0].strip().lower()
|
|
1023
|
+
continue
|
|
1024
|
+
if section != "user" or "=" not in line:
|
|
1025
|
+
continue
|
|
1026
|
+
key, _, value = line.partition("=")
|
|
1027
|
+
key = key.strip().lower()
|
|
1028
|
+
value = value.strip().strip('"')
|
|
1029
|
+
if key == "name" and name is None:
|
|
1030
|
+
name = value
|
|
1031
|
+
elif key == "email" and email is None:
|
|
1032
|
+
email = value
|
|
1033
|
+
except OSError:
|
|
1034
|
+
return None, None
|
|
1035
|
+
return name, email
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def seed_identity(repo, ws=None):
|
|
1039
|
+
"""Copy the project's git identity into the workspace, at creation.
|
|
1040
|
+
|
|
1041
|
+
The workspace is tezgah's own repository, so its commits have to carry the
|
|
1042
|
+
person who owns the code: without this, a machine whose global identity is
|
|
1043
|
+
unset refuses the commit, and the earlier answer - forcing a name in our own
|
|
1044
|
+
argv - wrote a repository's history under a committer nobody holds. Read from
|
|
1045
|
+
the config files rather than through `git config`, so a session start forks no
|
|
1046
|
+
extra git (see `_git_config_paths`). Fail-open: no identity anywhere leaves git
|
|
1047
|
+
to say so, which is its own honest error."""
|
|
1048
|
+
ws = ws or workspace(repo)
|
|
1049
|
+
found = {}
|
|
1050
|
+
for path in _git_config_paths(repo):
|
|
1051
|
+
name, email = _identity_in(path)
|
|
1052
|
+
if name and "user.name" not in found:
|
|
1053
|
+
found["user.name"] = name
|
|
1054
|
+
if email and "user.email" not in found:
|
|
1055
|
+
found["user.email"] = email
|
|
1056
|
+
if len(found) == len(IDENTITY_KEYS):
|
|
1057
|
+
break
|
|
1058
|
+
if not found:
|
|
1059
|
+
return
|
|
1060
|
+
path = os.path.join(ws, ".git", "config")
|
|
1061
|
+
try:
|
|
1062
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
1063
|
+
body = fh.read()
|
|
1064
|
+
except OSError:
|
|
1065
|
+
return
|
|
1066
|
+
if "[user]" in body:
|
|
1067
|
+
return
|
|
1068
|
+
# inside the `[user]` section the key loses its prefix: `name`, not `user.name`
|
|
1069
|
+
block = "".join("\t%s = %s\n" % (key.split(".")[-1], found[key])
|
|
1070
|
+
for key in IDENTITY_KEYS if key in found)
|
|
1071
|
+
try:
|
|
1072
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
1073
|
+
fh.write("[user]\n" + block)
|
|
1074
|
+
except OSError:
|
|
1075
|
+
pass
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def ws_git(repo, *args, **kw):
|
|
1079
|
+
"""`git -C <repo>/.tezgah <args>` with tezgah's commit identity, as a
|
|
1080
|
+
CompletedProcess (text, captured unless `kw` says otherwise). Raises OSError
|
|
1081
|
+
when git is not installed, like subprocess.run."""
|
|
1082
|
+
import subprocess # deferred: see ensure_workspace
|
|
1083
|
+
kw.setdefault("capture_output", True)
|
|
1084
|
+
kw.setdefault("text", True)
|
|
1085
|
+
return subprocess.run(["git", "-C", workspace(repo)] + list(WS_IDENTITY)
|
|
1086
|
+
+ list(args), **kw)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def guarded_opener(drop_auth=False):
|
|
1090
|
+
"""An urllib opener whose redirects never carry the bearer off its origin.
|
|
1091
|
+
|
|
1092
|
+
urllib copies the Authorization header onto a redirected request, so a
|
|
1093
|
+
301/302 from a repointable endpoint would hand the key to whatever host the
|
|
1094
|
+
answer named - or, on a same-host https->http hop, send it in clear. A hop
|
|
1095
|
+
that changes the scheme or the host is refused (it surfaces as an
|
|
1096
|
+
HTTPError), or, with `drop_auth`, followed without the header so the far end
|
|
1097
|
+
answers 401 and the caller reports a key failure; a same-origin redirect is
|
|
1098
|
+
followed as urllib would. One copy for the judge seam and codegen (refuse)
|
|
1099
|
+
and consult (drop). The import is deferred: see the fallback_cache() note."""
|
|
1100
|
+
import urllib.parse
|
|
1101
|
+
import urllib.request
|
|
1102
|
+
|
|
1103
|
+
class Guard(urllib.request.HTTPRedirectHandler):
|
|
1104
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
1105
|
+
old, new_url = urllib.parse.urlsplit(req.full_url), urllib.parse.urlsplit(newurl)
|
|
1106
|
+
cross = (new_url.scheme.lower(), new_url.netloc) != (old.scheme.lower(), old.netloc)
|
|
1107
|
+
if cross and not drop_auth:
|
|
1108
|
+
return None
|
|
1109
|
+
new = super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
1110
|
+
if new is not None and cross:
|
|
1111
|
+
new.remove_header("Authorization")
|
|
1112
|
+
return new
|
|
1113
|
+
|
|
1114
|
+
return urllib.request.build_opener(Guard)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def plain_http(url):
|
|
1118
|
+
"""True for an `http://` endpoint off this machine: the bearer and the body
|
|
1119
|
+
would cross the network in clear. Loopback stays allowed - the tests and a
|
|
1120
|
+
local gateway point the endpoint overrides at `http://127.0.0.1`."""
|
|
1121
|
+
import ipaddress
|
|
1122
|
+
import urllib.parse
|
|
1123
|
+
parts = urllib.parse.urlsplit(url)
|
|
1124
|
+
if parts.scheme.lower() != "http":
|
|
1125
|
+
return False
|
|
1126
|
+
host = parts.hostname or ""
|
|
1127
|
+
if host == "localhost":
|
|
1128
|
+
return False
|
|
1129
|
+
try:
|
|
1130
|
+
return not ipaddress.ip_address(host).is_loopback
|
|
1131
|
+
except ValueError:
|
|
1132
|
+
return True
|