@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.
Files changed (582) hide show
  1. package/CHANGELOG.md +738 -0
  2. package/LICENSE +21 -0
  3. package/MANIFEST +753 -0
  4. package/README.de.md +169 -0
  5. package/README.es.md +168 -0
  6. package/README.fr.md +166 -0
  7. package/README.md +161 -0
  8. package/README.tr.md +163 -0
  9. package/agents/tezgah-reviewer.md +70 -0
  10. package/bin/codegen +337 -0
  11. package/bin/consult +797 -0
  12. package/bin/tezgah-adhd +50 -0
  13. package/bin/tezgah-agents +45 -0
  14. package/bin/tezgah-capture +66 -0
  15. package/bin/tezgah-context +96 -0
  16. package/bin/tezgah-decisions +213 -0
  17. package/bin/tezgah-design +953 -0
  18. package/bin/tezgah-docs +1199 -0
  19. package/bin/tezgah-doctor +497 -0
  20. package/bin/tezgah-dsh +37 -0
  21. package/bin/tezgah-dsh.cmd +70 -0
  22. package/bin/tezgah-gate +75 -0
  23. package/bin/tezgah-import-ai-research +439 -0
  24. package/bin/tezgah-index +32 -0
  25. package/bin/tezgah-lessons +148 -0
  26. package/bin/tezgah-mcp +322 -0
  27. package/bin/tezgah-migrate +324 -0
  28. package/bin/tezgah-pony +50 -0
  29. package/bin/tezgah-research +772 -0
  30. package/bin/tezgah-rollback +93 -0
  31. package/bin/tezgah-route +124 -0
  32. package/bin/tezgah-setup +5798 -0
  33. package/bin/tezgah-status +383 -0
  34. package/bin/tezgah-task +607 -0
  35. package/bin/tezgah-taste +423 -0
  36. package/bin/tezgah-triage +485 -0
  37. package/bin/tezgah.js +36 -0
  38. package/commands/adhd.md +21 -0
  39. package/commands/ponytail.md +21 -0
  40. package/docs/README.md +112 -0
  41. package/docs/architecture.md +226 -0
  42. package/docs/citations-baseline.json +59 -0
  43. package/docs/clarity-baseline.json +116 -0
  44. package/docs/contract.md +416 -0
  45. package/docs/environment.md +141 -0
  46. package/docs/evidence.md +831 -0
  47. package/docs/feature-audit.md +139 -0
  48. package/docs/gate.md +407 -0
  49. package/docs/glossary.md +201 -0
  50. package/docs/hosts.md +400 -0
  51. package/docs/index.json +471 -0
  52. package/docs/judge.md +193 -0
  53. package/docs/layers.md +189 -0
  54. package/docs/models.md +160 -0
  55. package/docs/operations.md +666 -0
  56. package/docs/research.md +809 -0
  57. package/docs/results/paired-outcome-2026-10.jsonl +254 -0
  58. package/docs/results/paired-outcome-2026-10.md +204 -0
  59. package/docs/skills.md +236 -0
  60. package/docs/status-line.md +315 -0
  61. package/docs/testing.md +287 -0
  62. package/docs/vision.md +117 -0
  63. package/hooks/hooks.json +33 -0
  64. package/hooks/projects-auto-init.py +98 -0
  65. package/hooks/projects-posttooluse.py +149 -0
  66. package/hooks/projects-pretooluse.py +40 -0
  67. package/hooks/projects-stop.py +60 -0
  68. package/hooks/tezgah_agents.py +817 -0
  69. package/hooks/tezgah_apps.py +266 -0
  70. package/hooks/tezgah_attest.py +255 -0
  71. package/hooks/tezgah_clarity.py +171 -0
  72. package/hooks/tezgah_components.py +140 -0
  73. package/hooks/tezgah_context.py +2953 -0
  74. package/hooks/tezgah_embed.py +765 -0
  75. package/hooks/tezgah_gate.py +2736 -0
  76. package/hooks/tezgah_guard.py +177 -0
  77. package/hooks/tezgah_index.py +124 -0
  78. package/hooks/tezgah_integrity.py +5141 -0
  79. package/hooks/tezgah_judge.py +487 -0
  80. package/hooks/tezgah_lang.py +230 -0
  81. package/hooks/tezgah_lessons.py +210 -0
  82. package/hooks/tezgah_models.py +855 -0
  83. package/hooks/tezgah_paths.py +1132 -0
  84. package/hooks/tezgah_policy.py +1039 -0
  85. package/hooks/tezgah_rank.py +77 -0
  86. package/hooks/tezgah_replay.py +1109 -0
  87. package/hooks/tezgah_research.py +5176 -0
  88. package/hooks/tezgah_shapes.py +305 -0
  89. package/hooks/tezgah_skill_pick.py +227 -0
  90. package/hooks/tezgah_snapshot.py +490 -0
  91. package/hooks/tezgah_stopspec.py +320 -0
  92. package/hooks/tezgah_task.py +458 -0
  93. package/hooks/tezgah_taste.py +201 -0
  94. package/hooks/tezgah_untrusted.py +112 -0
  95. package/hooks/tezgah_update.py +430 -0
  96. package/hosts/codex/hook.py +268 -0
  97. package/hosts/codex/hooks.json +26 -0
  98. package/hosts/cursor/hook.py +461 -0
  99. package/hosts/cursor/hooks.json +44 -0
  100. package/hosts/dsh/hooks.json +21 -0
  101. package/hosts/dsh/statusline/lib/client.js +138 -0
  102. package/hosts/dsh/statusline/lib/index.js +104 -0
  103. package/hosts/dsh/statusline/package.json +28 -0
  104. package/hosts/omp/hook.py +277 -0
  105. package/hosts/omp/tezgah-hook.ts.in +701 -0
  106. package/hosts/opencode/plugins/tezgah.js +2742 -0
  107. package/hosts/opencode/tui/tezgah-tui.tsx +151 -0
  108. package/output-styles/tezgah.md +142 -0
  109. package/package.json +32 -0
  110. package/packaging/build.sh +342 -0
  111. package/packaging/install.ps1 +166 -0
  112. package/packaging/install.sh +56 -0
  113. package/packaging/upgrade.sh +142 -0
  114. package/skills/ai-research/0-autoresearch-skill/SKILL.md +411 -0
  115. package/skills/ai-research/0-autoresearch-skill/references/agent-continuity.md +95 -0
  116. package/skills/ai-research/0-autoresearch-skill/references/progress-reporting.md +166 -0
  117. package/skills/ai-research/0-autoresearch-skill/references/skill-routing.md +219 -0
  118. package/skills/ai-research/0-autoresearch-skill/templates/findings.md +44 -0
  119. package/skills/ai-research/0-autoresearch-skill/templates/research-log.md +41 -0
  120. package/skills/ai-research/01-model-architecture/litgpt/SKILL.md +469 -0
  121. package/skills/ai-research/01-model-architecture/litgpt/references/custom-models.md +569 -0
  122. package/skills/ai-research/01-model-architecture/litgpt/references/distributed-training.md +452 -0
  123. package/skills/ai-research/01-model-architecture/litgpt/references/supported-models.md +337 -0
  124. package/skills/ai-research/01-model-architecture/litgpt/references/training-recipes.md +620 -0
  125. package/skills/ai-research/01-model-architecture/mamba/SKILL.md +260 -0
  126. package/skills/ai-research/01-model-architecture/mamba/references/architecture-details.md +207 -0
  127. package/skills/ai-research/01-model-architecture/mamba/references/benchmarks.md +256 -0
  128. package/skills/ai-research/01-model-architecture/mamba/references/training-guide.md +389 -0
  129. package/skills/ai-research/01-model-architecture/nanogpt/SKILL.md +290 -0
  130. package/skills/ai-research/01-model-architecture/nanogpt/references/architecture.md +383 -0
  131. package/skills/ai-research/01-model-architecture/nanogpt/references/data.md +477 -0
  132. package/skills/ai-research/01-model-architecture/nanogpt/references/training.md +565 -0
  133. package/skills/ai-research/01-model-architecture/rwkv/SKILL.md +260 -0
  134. package/skills/ai-research/01-model-architecture/rwkv/references/architecture-details.md +345 -0
  135. package/skills/ai-research/01-model-architecture/rwkv/references/rwkv7.md +387 -0
  136. package/skills/ai-research/01-model-architecture/rwkv/references/state-management.md +370 -0
  137. package/skills/ai-research/01-model-architecture/torchtitan/SKILL.md +358 -0
  138. package/skills/ai-research/01-model-architecture/torchtitan/references/checkpoint.md +182 -0
  139. package/skills/ai-research/01-model-architecture/torchtitan/references/custom-models.md +259 -0
  140. package/skills/ai-research/01-model-architecture/torchtitan/references/float8.md +134 -0
  141. package/skills/ai-research/01-model-architecture/torchtitan/references/fsdp.md +127 -0
  142. package/skills/ai-research/02-tokenization/huggingface-tokenizers/SKILL.md +516 -0
  143. package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/algorithms.md +654 -0
  144. package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/integration.md +638 -0
  145. package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/pipeline.md +724 -0
  146. package/skills/ai-research/02-tokenization/huggingface-tokenizers/references/training.md +566 -0
  147. package/skills/ai-research/02-tokenization/sentencepiece/SKILL.md +235 -0
  148. package/skills/ai-research/02-tokenization/sentencepiece/references/algorithms.md +201 -0
  149. package/skills/ai-research/02-tokenization/sentencepiece/references/training.md +305 -0
  150. package/skills/ai-research/03-fine-tuning/axolotl/SKILL.md +158 -0
  151. package/skills/ai-research/03-fine-tuning/axolotl/references/api.md +5549 -0
  152. package/skills/ai-research/03-fine-tuning/axolotl/references/dataset-formats.md +1030 -0
  153. package/skills/ai-research/03-fine-tuning/axolotl/references/index.md +16 -0
  154. package/skills/ai-research/03-fine-tuning/axolotl/references/other.md +3564 -0
  155. package/skills/ai-research/03-fine-tuning/llama-factory/SKILL.md +80 -0
  156. package/skills/ai-research/03-fine-tuning/llama-factory/references/_images.md +24 -0
  157. package/skills/ai-research/03-fine-tuning/llama-factory/references/advanced.md +1056 -0
  158. package/skills/ai-research/03-fine-tuning/llama-factory/references/getting_started.md +350 -0
  159. package/skills/ai-research/03-fine-tuning/llama-factory/references/index.md +20 -0
  160. package/skills/ai-research/03-fine-tuning/llama-factory/references/other.md +32 -0
  161. package/skills/ai-research/03-fine-tuning/peft/SKILL.md +431 -0
  162. package/skills/ai-research/03-fine-tuning/peft/references/advanced-usage.md +515 -0
  163. package/skills/ai-research/03-fine-tuning/peft/references/troubleshooting.md +481 -0
  164. package/skills/ai-research/03-fine-tuning/unsloth/SKILL.md +80 -0
  165. package/skills/ai-research/03-fine-tuning/unsloth/references/index.md +8 -0
  166. package/skills/ai-research/03-fine-tuning/unsloth/references/llms.md +83 -0
  167. package/skills/ai-research/04-mechanistic-interpretability/nnsight/SKILL.md +436 -0
  168. package/skills/ai-research/04-mechanistic-interpretability/nnsight/references/README.md +79 -0
  169. package/skills/ai-research/04-mechanistic-interpretability/nnsight/references/api.md +345 -0
  170. package/skills/ai-research/04-mechanistic-interpretability/nnsight/references/tutorials.md +301 -0
  171. package/skills/ai-research/04-mechanistic-interpretability/pyvene/SKILL.md +473 -0
  172. package/skills/ai-research/04-mechanistic-interpretability/pyvene/references/README.md +74 -0
  173. package/skills/ai-research/04-mechanistic-interpretability/pyvene/references/api.md +384 -0
  174. package/skills/ai-research/04-mechanistic-interpretability/pyvene/references/tutorials.md +377 -0
  175. package/skills/ai-research/04-mechanistic-interpretability/saelens/SKILL.md +386 -0
  176. package/skills/ai-research/04-mechanistic-interpretability/saelens/references/README.md +71 -0
  177. package/skills/ai-research/04-mechanistic-interpretability/saelens/references/api.md +334 -0
  178. package/skills/ai-research/04-mechanistic-interpretability/saelens/references/tutorials.md +319 -0
  179. package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/SKILL.md +346 -0
  180. package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/references/README.md +55 -0
  181. package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/references/api.md +363 -0
  182. package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/references/tutorials.md +340 -0
  183. package/skills/ai-research/05-data-processing/nemo-curator/SKILL.md +383 -0
  184. package/skills/ai-research/05-data-processing/nemo-curator/references/deduplication.md +88 -0
  185. package/skills/ai-research/05-data-processing/nemo-curator/references/filtering.md +103 -0
  186. package/skills/ai-research/05-data-processing/ray-data/SKILL.md +326 -0
  187. package/skills/ai-research/05-data-processing/ray-data/references/integration.md +83 -0
  188. package/skills/ai-research/05-data-processing/ray-data/references/transformations.md +84 -0
  189. package/skills/ai-research/06-post-training/grpo-rl-training/README.md +98 -0
  190. package/skills/ai-research/06-post-training/grpo-rl-training/SKILL.md +572 -0
  191. package/skills/ai-research/06-post-training/miles/SKILL.md +315 -0
  192. package/skills/ai-research/06-post-training/miles/references/api-reference.md +142 -0
  193. package/skills/ai-research/06-post-training/miles/references/troubleshooting.md +353 -0
  194. package/skills/ai-research/06-post-training/openrlhf/SKILL.md +249 -0
  195. package/skills/ai-research/06-post-training/openrlhf/references/algorithm-comparison.md +405 -0
  196. package/skills/ai-research/06-post-training/openrlhf/references/custom-rewards.md +531 -0
  197. package/skills/ai-research/06-post-training/openrlhf/references/hybrid-engine.md +288 -0
  198. package/skills/ai-research/06-post-training/openrlhf/references/multi-node-training.md +455 -0
  199. package/skills/ai-research/06-post-training/simpo/SKILL.md +219 -0
  200. package/skills/ai-research/06-post-training/simpo/references/datasets.md +479 -0
  201. package/skills/ai-research/06-post-training/simpo/references/hyperparameters.md +453 -0
  202. package/skills/ai-research/06-post-training/simpo/references/loss-functions.md +351 -0
  203. package/skills/ai-research/06-post-training/slime/SKILL.md +464 -0
  204. package/skills/ai-research/06-post-training/slime/references/api-reference.md +393 -0
  205. package/skills/ai-research/06-post-training/slime/references/troubleshooting.md +387 -0
  206. package/skills/ai-research/06-post-training/torchforge/SKILL.md +433 -0
  207. package/skills/ai-research/06-post-training/torchforge/references/api-reference.md +328 -0
  208. package/skills/ai-research/06-post-training/torchforge/references/troubleshooting.md +410 -0
  209. package/skills/ai-research/06-post-training/trl-fine-tuning/SKILL.md +455 -0
  210. package/skills/ai-research/06-post-training/trl-fine-tuning/references/dpo-variants.md +228 -0
  211. package/skills/ai-research/06-post-training/trl-fine-tuning/references/online-rl.md +83 -0
  212. package/skills/ai-research/06-post-training/trl-fine-tuning/references/reward-modeling.md +123 -0
  213. package/skills/ai-research/06-post-training/trl-fine-tuning/references/sft-training.md +169 -0
  214. package/skills/ai-research/06-post-training/verl/SKILL.md +391 -0
  215. package/skills/ai-research/06-post-training/verl/references/api-reference.md +302 -0
  216. package/skills/ai-research/06-post-training/verl/references/troubleshooting.md +392 -0
  217. package/skills/ai-research/07-safety-alignment/constitutional-ai/SKILL.md +290 -0
  218. package/skills/ai-research/07-safety-alignment/llamaguard/SKILL.md +337 -0
  219. package/skills/ai-research/07-safety-alignment/nemo-guardrails/SKILL.md +297 -0
  220. package/skills/ai-research/07-safety-alignment/prompt-guard/SKILL.md +312 -0
  221. package/skills/ai-research/08-distributed-training/accelerate/SKILL.md +332 -0
  222. package/skills/ai-research/08-distributed-training/accelerate/references/custom-plugins.md +454 -0
  223. package/skills/ai-research/08-distributed-training/accelerate/references/megatron-integration.md +490 -0
  224. package/skills/ai-research/08-distributed-training/accelerate/references/performance.md +526 -0
  225. package/skills/ai-research/08-distributed-training/deepspeed/SKILL.md +141 -0
  226. package/skills/ai-research/08-distributed-training/deepspeed/references/08.md +18 -0
  227. package/skills/ai-research/08-distributed-training/deepspeed/references/09.md +174 -0
  228. package/skills/ai-research/08-distributed-training/deepspeed/references/2020.md +379 -0
  229. package/skills/ai-research/08-distributed-training/deepspeed/references/2023.md +280 -0
  230. package/skills/ai-research/08-distributed-training/deepspeed/references/assets.md +180 -0
  231. package/skills/ai-research/08-distributed-training/deepspeed/references/index.md +36 -0
  232. package/skills/ai-research/08-distributed-training/deepspeed/references/mii.md +119 -0
  233. package/skills/ai-research/08-distributed-training/deepspeed/references/other.md +1192 -0
  234. package/skills/ai-research/08-distributed-training/megatron-core/SKILL.md +366 -0
  235. package/skills/ai-research/08-distributed-training/megatron-core/references/benchmarks.md +250 -0
  236. package/skills/ai-research/08-distributed-training/megatron-core/references/parallelism-guide.md +405 -0
  237. package/skills/ai-research/08-distributed-training/megatron-core/references/production-examples.md +474 -0
  238. package/skills/ai-research/08-distributed-training/megatron-core/references/training-recipes.md +548 -0
  239. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/SKILL.md +231 -0
  240. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_dcp_async_recipe.md +14 -0
  241. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_dcp_overview.md +19 -0
  242. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_dcp_recipe.md +18 -0
  243. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_ddp_notes.md +10 -0
  244. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_device_mesh_tutorial.md +22 -0
  245. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_examples_fsdp2.md +15 -0
  246. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_fsdp1_api.md +9 -0
  247. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_fsdp2_tutorial.md +45 -0
  248. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_fully_shard_api.md +49 -0
  249. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/pytorch_tp_tutorial.md +22 -0
  250. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/ray_train_fsdp2_example.md +13 -0
  251. package/skills/ai-research/08-distributed-training/pytorch-fsdp2/references/torchtitan_fsdp_notes.md +14 -0
  252. package/skills/ai-research/08-distributed-training/pytorch-lightning/SKILL.md +346 -0
  253. package/skills/ai-research/08-distributed-training/pytorch-lightning/references/callbacks.md +437 -0
  254. package/skills/ai-research/08-distributed-training/pytorch-lightning/references/distributed.md +491 -0
  255. package/skills/ai-research/08-distributed-training/pytorch-lightning/references/hyperparameter-tuning.md +557 -0
  256. package/skills/ai-research/08-distributed-training/ray-train/SKILL.md +406 -0
  257. package/skills/ai-research/08-distributed-training/ray-train/references/multi-node.md +629 -0
  258. package/skills/ai-research/09-infrastructure/lambda-labs/SKILL.md +545 -0
  259. package/skills/ai-research/09-infrastructure/lambda-labs/references/advanced-usage.md +612 -0
  260. package/skills/ai-research/09-infrastructure/lambda-labs/references/troubleshooting.md +531 -0
  261. package/skills/ai-research/09-infrastructure/modal/SKILL.md +341 -0
  262. package/skills/ai-research/09-infrastructure/modal/references/advanced-usage.md +504 -0
  263. package/skills/ai-research/09-infrastructure/modal/references/troubleshooting.md +495 -0
  264. package/skills/ai-research/09-infrastructure/skypilot/SKILL.md +509 -0
  265. package/skills/ai-research/09-infrastructure/skypilot/references/advanced-usage.md +492 -0
  266. package/skills/ai-research/09-infrastructure/skypilot/references/troubleshooting.md +571 -0
  267. package/skills/ai-research/10-optimization/awq/SKILL.md +310 -0
  268. package/skills/ai-research/10-optimization/awq/references/advanced-usage.md +325 -0
  269. package/skills/ai-research/10-optimization/awq/references/troubleshooting.md +345 -0
  270. package/skills/ai-research/10-optimization/bitsandbytes/SKILL.md +411 -0
  271. package/skills/ai-research/10-optimization/bitsandbytes/references/memory-optimization.md +522 -0
  272. package/skills/ai-research/10-optimization/bitsandbytes/references/qlora-training.md +522 -0
  273. package/skills/ai-research/10-optimization/bitsandbytes/references/quantization-formats.md +448 -0
  274. package/skills/ai-research/10-optimization/flash-attention/SKILL.md +367 -0
  275. package/skills/ai-research/10-optimization/flash-attention/references/benchmarks.md +216 -0
  276. package/skills/ai-research/10-optimization/flash-attention/references/transformers-integration.md +294 -0
  277. package/skills/ai-research/10-optimization/gguf/SKILL.md +427 -0
  278. package/skills/ai-research/10-optimization/gguf/references/advanced-usage.md +505 -0
  279. package/skills/ai-research/10-optimization/gguf/references/troubleshooting.md +443 -0
  280. package/skills/ai-research/10-optimization/gptq/SKILL.md +450 -0
  281. package/skills/ai-research/10-optimization/gptq/references/calibration.md +338 -0
  282. package/skills/ai-research/10-optimization/gptq/references/integration.md +130 -0
  283. package/skills/ai-research/10-optimization/gptq/references/troubleshooting.md +96 -0
  284. package/skills/ai-research/10-optimization/hqq/SKILL.md +445 -0
  285. package/skills/ai-research/10-optimization/hqq/references/advanced-usage.md +529 -0
  286. package/skills/ai-research/10-optimization/hqq/references/troubleshooting.md +504 -0
  287. package/skills/ai-research/10-optimization/ml-training-recipes/SKILL.md +319 -0
  288. package/skills/ai-research/10-optimization/ml-training-recipes/references/architecture.md +320 -0
  289. package/skills/ai-research/10-optimization/ml-training-recipes/references/biomedical.md +498 -0
  290. package/skills/ai-research/10-optimization/ml-training-recipes/references/domain-specific.md +626 -0
  291. package/skills/ai-research/10-optimization/ml-training-recipes/references/experiment-loop.md +129 -0
  292. package/skills/ai-research/10-optimization/ml-training-recipes/references/optimizers.md +330 -0
  293. package/skills/ai-research/10-optimization/ml-training-recipes/references/scaling-and-selection.md +428 -0
  294. package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/SKILL.md +405 -0
  295. package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/references/benchmarks.md +394 -0
  296. package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/references/custom-tasks.md +425 -0
  297. package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/references/issues.md +395 -0
  298. package/skills/ai-research/11-evaluation/lm-evaluation-harness/SKILL.md +490 -0
  299. package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/api-evaluation.md +491 -0
  300. package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/benchmark-guide.md +489 -0
  301. package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/custom-tasks.md +603 -0
  302. package/skills/ai-research/11-evaluation/lm-evaluation-harness/references/distributed-eval.md +520 -0
  303. package/skills/ai-research/11-evaluation/nemo-evaluator/SKILL.md +494 -0
  304. package/skills/ai-research/11-evaluation/nemo-evaluator/references/adapter-system.md +341 -0
  305. package/skills/ai-research/11-evaluation/nemo-evaluator/references/configuration.md +448 -0
  306. package/skills/ai-research/11-evaluation/nemo-evaluator/references/custom-benchmarks.md +316 -0
  307. package/skills/ai-research/11-evaluation/nemo-evaluator/references/execution-backends.md +362 -0
  308. package/skills/ai-research/12-inference-serving/llama-cpp/SKILL.md +258 -0
  309. package/skills/ai-research/12-inference-serving/llama-cpp/references/optimization.md +90 -0
  310. package/skills/ai-research/12-inference-serving/llama-cpp/references/quantization.md +214 -0
  311. package/skills/ai-research/12-inference-serving/llama-cpp/references/server.md +126 -0
  312. package/skills/ai-research/12-inference-serving/sglang/SKILL.md +442 -0
  313. package/skills/ai-research/12-inference-serving/sglang/references/deployment.md +491 -0
  314. package/skills/ai-research/12-inference-serving/sglang/references/radix-attention.md +414 -0
  315. package/skills/ai-research/12-inference-serving/sglang/references/structured-generation.md +542 -0
  316. package/skills/ai-research/12-inference-serving/tensorrt-llm/SKILL.md +187 -0
  317. package/skills/ai-research/12-inference-serving/tensorrt-llm/references/multi-gpu.md +299 -0
  318. package/skills/ai-research/12-inference-serving/tensorrt-llm/references/optimization.md +243 -0
  319. package/skills/ai-research/12-inference-serving/tensorrt-llm/references/serving.md +471 -0
  320. package/skills/ai-research/12-inference-serving/vllm/SKILL.md +364 -0
  321. package/skills/ai-research/12-inference-serving/vllm/references/optimization.md +227 -0
  322. package/skills/ai-research/12-inference-serving/vllm/references/quantization.md +285 -0
  323. package/skills/ai-research/12-inference-serving/vllm/references/server-deployment.md +256 -0
  324. package/skills/ai-research/12-inference-serving/vllm/references/troubleshooting.md +448 -0
  325. package/skills/ai-research/13-mlops/mlflow/SKILL.md +704 -0
  326. package/skills/ai-research/13-mlops/mlflow/references/deployment.md +745 -0
  327. package/skills/ai-research/13-mlops/mlflow/references/model-registry.md +771 -0
  328. package/skills/ai-research/13-mlops/mlflow/references/tracking.md +681 -0
  329. package/skills/ai-research/13-mlops/swanlab/SKILL.md +406 -0
  330. package/skills/ai-research/13-mlops/swanlab/references/integrations.md +305 -0
  331. package/skills/ai-research/13-mlops/swanlab/references/visualization.md +234 -0
  332. package/skills/ai-research/13-mlops/tensorboard/SKILL.md +629 -0
  333. package/skills/ai-research/13-mlops/tensorboard/references/integrations.md +639 -0
  334. package/skills/ai-research/13-mlops/tensorboard/references/profiling.md +546 -0
  335. package/skills/ai-research/13-mlops/tensorboard/references/visualization.md +621 -0
  336. package/skills/ai-research/13-mlops/weights-and-biases/SKILL.md +590 -0
  337. package/skills/ai-research/13-mlops/weights-and-biases/references/artifacts.md +585 -0
  338. package/skills/ai-research/13-mlops/weights-and-biases/references/integrations.md +701 -0
  339. package/skills/ai-research/13-mlops/weights-and-biases/references/sweeps.md +848 -0
  340. package/skills/ai-research/14-agents/a-evolve/SKILL.md +383 -0
  341. package/skills/ai-research/14-agents/a-evolve/references/README.md +831 -0
  342. package/skills/ai-research/14-agents/a-evolve/references/api.md +594 -0
  343. package/skills/ai-research/14-agents/a-evolve/references/architecture.md +417 -0
  344. package/skills/ai-research/14-agents/a-evolve/references/design-patterns.md +462 -0
  345. package/skills/ai-research/14-agents/a-evolve/references/examples.md +352 -0
  346. package/skills/ai-research/14-agents/a-evolve/references/issues.md +591 -0
  347. package/skills/ai-research/14-agents/a-evolve/references/releases.md +112 -0
  348. package/skills/ai-research/14-agents/a-evolve/references/tutorials.md +819 -0
  349. package/skills/ai-research/14-agents/autogpt/SKILL.md +403 -0
  350. package/skills/ai-research/14-agents/autogpt/references/advanced-usage.md +536 -0
  351. package/skills/ai-research/14-agents/autogpt/references/troubleshooting.md +421 -0
  352. package/skills/ai-research/14-agents/crewai/SKILL.md +498 -0
  353. package/skills/ai-research/14-agents/crewai/references/flows.md +439 -0
  354. package/skills/ai-research/14-agents/crewai/references/tools.md +430 -0
  355. package/skills/ai-research/14-agents/crewai/references/troubleshooting.md +481 -0
  356. package/skills/ai-research/14-agents/langchain/SKILL.md +480 -0
  357. package/skills/ai-research/14-agents/langchain/references/agents.md +500 -0
  358. package/skills/ai-research/14-agents/langchain/references/integration.md +563 -0
  359. package/skills/ai-research/14-agents/langchain/references/rag.md +601 -0
  360. package/skills/ai-research/14-agents/llamaindex/SKILL.md +569 -0
  361. package/skills/ai-research/14-agents/llamaindex/references/agents.md +84 -0
  362. package/skills/ai-research/14-agents/llamaindex/references/data_connectors.md +109 -0
  363. package/skills/ai-research/14-agents/llamaindex/references/query_engines.md +407 -0
  364. package/skills/ai-research/15-rag/chroma/SKILL.md +406 -0
  365. package/skills/ai-research/15-rag/chroma/references/integration.md +39 -0
  366. package/skills/ai-research/15-rag/faiss/SKILL.md +221 -0
  367. package/skills/ai-research/15-rag/faiss/references/index_types.md +281 -0
  368. package/skills/ai-research/15-rag/pinecone/SKILL.md +358 -0
  369. package/skills/ai-research/15-rag/pinecone/references/deployment.md +182 -0
  370. package/skills/ai-research/15-rag/qdrant/SKILL.md +493 -0
  371. package/skills/ai-research/15-rag/qdrant/references/advanced-usage.md +649 -0
  372. package/skills/ai-research/15-rag/qdrant/references/troubleshooting.md +632 -0
  373. package/skills/ai-research/15-rag/sentence-transformers/SKILL.md +255 -0
  374. package/skills/ai-research/15-rag/sentence-transformers/references/models.md +124 -0
  375. package/skills/ai-research/16-prompt-engineering/dspy/SKILL.md +590 -0
  376. package/skills/ai-research/16-prompt-engineering/dspy/references/examples.md +664 -0
  377. package/skills/ai-research/16-prompt-engineering/dspy/references/modules.md +476 -0
  378. package/skills/ai-research/16-prompt-engineering/dspy/references/optimizers.md +567 -0
  379. package/skills/ai-research/16-prompt-engineering/guidance/SKILL.md +572 -0
  380. package/skills/ai-research/16-prompt-engineering/guidance/references/backends.md +555 -0
  381. package/skills/ai-research/16-prompt-engineering/guidance/references/constraints.md +675 -0
  382. package/skills/ai-research/16-prompt-engineering/guidance/references/examples.md +768 -0
  383. package/skills/ai-research/16-prompt-engineering/instructor/SKILL.md +740 -0
  384. package/skills/ai-research/16-prompt-engineering/instructor/references/examples.md +108 -0
  385. package/skills/ai-research/16-prompt-engineering/instructor/references/providers.md +71 -0
  386. package/skills/ai-research/16-prompt-engineering/instructor/references/validation.md +607 -0
  387. package/skills/ai-research/16-prompt-engineering/outlines/SKILL.md +652 -0
  388. package/skills/ai-research/16-prompt-engineering/outlines/references/backends.md +616 -0
  389. package/skills/ai-research/16-prompt-engineering/outlines/references/examples.md +774 -0
  390. package/skills/ai-research/16-prompt-engineering/outlines/references/json_generation.md +653 -0
  391. package/skills/ai-research/17-observability/langsmith/SKILL.md +422 -0
  392. package/skills/ai-research/17-observability/langsmith/references/advanced-usage.md +549 -0
  393. package/skills/ai-research/17-observability/langsmith/references/troubleshooting.md +538 -0
  394. package/skills/ai-research/17-observability/phoenix/SKILL.md +475 -0
  395. package/skills/ai-research/17-observability/phoenix/references/advanced-usage.md +620 -0
  396. package/skills/ai-research/17-observability/phoenix/references/troubleshooting.md +539 -0
  397. package/skills/ai-research/18-multimodal/audiocraft/SKILL.md +564 -0
  398. package/skills/ai-research/18-multimodal/audiocraft/references/advanced-usage.md +667 -0
  399. package/skills/ai-research/18-multimodal/audiocraft/references/troubleshooting.md +505 -0
  400. package/skills/ai-research/18-multimodal/blip-2/SKILL.md +564 -0
  401. package/skills/ai-research/18-multimodal/blip-2/references/advanced-usage.md +681 -0
  402. package/skills/ai-research/18-multimodal/blip-2/references/troubleshooting.md +527 -0
  403. package/skills/ai-research/18-multimodal/clip/SKILL.md +253 -0
  404. package/skills/ai-research/18-multimodal/clip/references/applications.md +208 -0
  405. package/skills/ai-research/18-multimodal/cosmos-policy/SKILL.md +402 -0
  406. package/skills/ai-research/18-multimodal/cosmos-policy/references/libero-commands.md +175 -0
  407. package/skills/ai-research/18-multimodal/cosmos-policy/references/robocasa-commands.md +147 -0
  408. package/skills/ai-research/18-multimodal/llava/SKILL.md +304 -0
  409. package/skills/ai-research/18-multimodal/llava/references/training.md +198 -0
  410. package/skills/ai-research/18-multimodal/openpi/SKILL.md +480 -0
  411. package/skills/ai-research/18-multimodal/openpi/references/checkpoints-and-env-map.md +86 -0
  412. package/skills/ai-research/18-multimodal/openpi/references/config-recipes.md +50 -0
  413. package/skills/ai-research/18-multimodal/openpi/references/pytorch-gotchas.md +49 -0
  414. package/skills/ai-research/18-multimodal/openpi/references/remote-client-pattern.md +48 -0
  415. package/skills/ai-research/18-multimodal/openpi/references/training-debugging.md +46 -0
  416. package/skills/ai-research/18-multimodal/openvla-oft/SKILL.md +441 -0
  417. package/skills/ai-research/18-multimodal/openvla-oft/references/aloha-workflow.md +141 -0
  418. package/skills/ai-research/18-multimodal/openvla-oft/references/config-troubleshooting.md +97 -0
  419. package/skills/ai-research/18-multimodal/openvla-oft/references/libero-workflow.md +115 -0
  420. package/skills/ai-research/18-multimodal/openvla-oft/references/paper-and-checkpoints.md +84 -0
  421. package/skills/ai-research/18-multimodal/segment-anything/SKILL.md +500 -0
  422. package/skills/ai-research/18-multimodal/segment-anything/references/advanced-usage.md +590 -0
  423. package/skills/ai-research/18-multimodal/segment-anything/references/troubleshooting.md +485 -0
  424. package/skills/ai-research/18-multimodal/stable-diffusion/SKILL.md +519 -0
  425. package/skills/ai-research/18-multimodal/stable-diffusion/references/advanced-usage.md +717 -0
  426. package/skills/ai-research/18-multimodal/stable-diffusion/references/troubleshooting.md +556 -0
  427. package/skills/ai-research/18-multimodal/whisper/SKILL.md +317 -0
  428. package/skills/ai-research/18-multimodal/whisper/references/languages.md +190 -0
  429. package/skills/ai-research/19-emerging-techniques/knowledge-distillation/SKILL.md +458 -0
  430. package/skills/ai-research/19-emerging-techniques/knowledge-distillation/references/minillm.md +335 -0
  431. package/skills/ai-research/19-emerging-techniques/long-context/SKILL.md +536 -0
  432. package/skills/ai-research/19-emerging-techniques/long-context/references/extension_methods.md +469 -0
  433. package/skills/ai-research/19-emerging-techniques/long-context/references/fine_tuning.md +612 -0
  434. package/skills/ai-research/19-emerging-techniques/long-context/references/rope.md +403 -0
  435. package/skills/ai-research/19-emerging-techniques/model-merging/SKILL.md +497 -0
  436. package/skills/ai-research/19-emerging-techniques/model-merging/references/coefficient-tuning.md +415 -0
  437. package/skills/ai-research/19-emerging-techniques/model-merging/references/evaluation.md +463 -0
  438. package/skills/ai-research/19-emerging-techniques/model-merging/references/examples.md +429 -0
  439. package/skills/ai-research/19-emerging-techniques/model-merging/references/methods.md +353 -0
  440. package/skills/ai-research/19-emerging-techniques/model-pruning/SKILL.md +495 -0
  441. package/skills/ai-research/19-emerging-techniques/model-pruning/references/wanda.md +348 -0
  442. package/skills/ai-research/19-emerging-techniques/moe-training/SKILL.md +526 -0
  443. package/skills/ai-research/19-emerging-techniques/moe-training/references/architectures.md +433 -0
  444. package/skills/ai-research/19-emerging-techniques/moe-training/references/inference.md +349 -0
  445. package/skills/ai-research/19-emerging-techniques/moe-training/references/training.md +426 -0
  446. package/skills/ai-research/19-emerging-techniques/speculative-decoding/SKILL.md +467 -0
  447. package/skills/ai-research/19-emerging-techniques/speculative-decoding/references/lookahead.md +310 -0
  448. package/skills/ai-research/19-emerging-techniques/speculative-decoding/references/medusa.md +351 -0
  449. package/skills/ai-research/20-ml-paper-writing/academic-plotting/SKILL.md +479 -0
  450. package/skills/ai-research/20-ml-paper-writing/academic-plotting/references/data-visualization.md +509 -0
  451. package/skills/ai-research/20-ml-paper-writing/academic-plotting/references/diagram-generation.md +395 -0
  452. package/skills/ai-research/20-ml-paper-writing/academic-plotting/references/style-guide.md +305 -0
  453. package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/SKILL.md +982 -0
  454. package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/checklists.md +348 -0
  455. package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/citation-workflow.md +563 -0
  456. package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/reviewer-guidelines.md +349 -0
  457. package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/sources.md +165 -0
  458. package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/references/writing-guide.md +477 -0
  459. package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/SKILL.md +298 -0
  460. package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/references/slide-templates.md +505 -0
  461. package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/SKILL.md +270 -0
  462. package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/checklist.md +279 -0
  463. package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/reviewer-guidelines.md +116 -0
  464. package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/section-blueprints.md +400 -0
  465. package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/systems-conferences.md +261 -0
  466. package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/references/writing-patterns.md +231 -0
  467. package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/SKILL.md +384 -0
  468. package/skills/ai-research/21-research-ideation/creative-thinking-for-research/SKILL.md +366 -0
  469. package/skills/ai-research/22-agent-native-research-artifact/compiler/SKILL.md +245 -0
  470. package/skills/ai-research/22-agent-native-research-artifact/compiler/references/ara-schema.md +439 -0
  471. package/skills/ai-research/22-agent-native-research-artifact/compiler/references/exploration-tree-spec.md +125 -0
  472. package/skills/ai-research/22-agent-native-research-artifact/compiler/references/validation-checklist.md +149 -0
  473. package/skills/ai-research/22-agent-native-research-artifact/research-manager/SKILL.md +324 -0
  474. package/skills/ai-research/22-agent-native-research-artifact/research-manager/references/event-taxonomy.md +124 -0
  475. package/skills/ai-research/22-agent-native-research-artifact/research-manager/references/provenance-tags.md +190 -0
  476. package/skills/ai-research/22-agent-native-research-artifact/research-manager/references/session-protocol.md +162 -0
  477. package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/SKILL.md +322 -0
  478. package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/references/review-dimensions.md +182 -0
  479. package/skills/ai-research/SKILL.md +101 -0
  480. package/skills/ai-research/SOURCE +38 -0
  481. package/skills/ai-research/index/1-frame.md +18 -0
  482. package/skills/ai-research/index/2-data.md +21 -0
  483. package/skills/ai-research/index/3-train.md +48 -0
  484. package/skills/ai-research/index/4-measure.md +25 -0
  485. package/skills/ai-research/index/5-run.md +23 -0
  486. package/skills/ai-research/index/6-write.md +35 -0
  487. package/skills/ai-research/library.json +3456 -0
  488. package/skills/analyze-app/EVALS.md +82 -0
  489. package/skills/analyze-app/SKILL.md +188 -0
  490. package/skills/design-contract/EVALS.md +80 -0
  491. package/skills/design-contract/SKILL.md +171 -0
  492. package/skills/design-library/EVALS.md +85 -0
  493. package/skills/design-library/INDEX.md +59 -0
  494. package/skills/design-library/SKILL.md +58 -0
  495. package/skills/design-library/SOURCE +131 -0
  496. package/skills/design-library/adaptive-interfaces/skills/colour-independence/SKILL.md +90 -0
  497. package/skills/design-library/adaptive-interfaces/skills/flexible-typography/SKILL.md +83 -0
  498. package/skills/design-library/adaptive-interfaces/skills/information-density/SKILL.md +87 -0
  499. package/skills/design-library/adaptive-interfaces/skills/preference-audit/SKILL.md +70 -0
  500. package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/SKILL.md +86 -0
  501. package/skills/design-library/adaptive-interfaces/skills/responsive-review/SKILL.md +76 -0
  502. package/skills/design-library/adaptive-interfaces/skills/simplified-views/SKILL.md +97 -0
  503. package/skills/design-library/adaptive-interfaces/skills/specify/SKILL.md +63 -0
  504. package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/SKILL.md +100 -0
  505. package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/SKILL.md +61 -0
  506. package/skills/design-library/cognitive-accessibility/skills/assess-load/SKILL.md +48 -0
  507. package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/SKILL.md +60 -0
  508. package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/SKILL.md +61 -0
  509. package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/SKILL.md +71 -0
  510. package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/SKILL.md +67 -0
  511. package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/SKILL.md +60 -0
  512. package/skills/design-library/cognitive-accessibility/skills/plain-language-design/SKILL.md +78 -0
  513. package/skills/design-library/cognitive-accessibility/skills/review/SKILL.md +56 -0
  514. package/skills/design-library/cognitive-accessibility/skills/simplify/SKILL.md +39 -0
  515. package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/SKILL.md +64 -0
  516. package/skills/design-library/design-research/skills/behavioural-analytics/SKILL.md +33 -0
  517. package/skills/design-library/design-research/skills/qual-quant-triangulation/SKILL.md +33 -0
  518. package/skills/design-library/inclusive-interaction/skills/audit/SKILL.md +50 -0
  519. package/skills/design-library/inclusive-interaction/skills/design-flow/SKILL.md +58 -0
  520. package/skills/design-library/inclusive-interaction/skills/feedback-and-status/SKILL.md +76 -0
  521. package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/SKILL.md +70 -0
  522. package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/SKILL.md +99 -0
  523. package/skills/design-library/inclusive-interaction/skills/keyboard-review/SKILL.md +54 -0
  524. package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/SKILL.md +79 -0
  525. package/skills/design-library/inclusive-interaction/skills/multi-modal-input/SKILL.md +70 -0
  526. package/skills/design-library/inclusive-interaction/skills/touch-target-design/SKILL.md +71 -0
  527. package/skills/design-library/inclusive-interaction/skills/voice-interaction/SKILL.md +68 -0
  528. package/skills/design-library/visual-critique/skills/critique-affordance/SKILL.md +45 -0
  529. package/skills/design-library/visual-critique/skills/critique-brand-consistency/SKILL.md +42 -0
  530. package/skills/design-library/visual-critique/skills/critique-color/SKILL.md +44 -0
  531. package/skills/design-library/visual-critique/skills/critique-composition/SKILL.md +46 -0
  532. package/skills/design-library/visual-critique/skills/critique-information-density/SKILL.md +45 -0
  533. package/skills/design-library/visual-critique/skills/critique-typography/SKILL.md +45 -0
  534. package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/SKILL.md +40 -0
  535. package/skills/feature-audit/EVALS.md +83 -0
  536. package/skills/feature-audit/SKILL.md +326 -0
  537. package/skills/harness/EVALS.md +81 -0
  538. package/skills/harness/SKILL.md +88 -0
  539. package/skills/i-have-adhd/EVALS.md +80 -0
  540. package/skills/i-have-adhd/SKILL.md +183 -0
  541. package/skills/no-ai-slop/EVALS.md +89 -0
  542. package/skills/no-ai-slop/SKILL.md +209 -0
  543. package/skills/no-ai-slop/eval.md +52 -0
  544. package/skills/plan-add/EVALS.md +79 -0
  545. package/skills/plan-add/SKILL.md +179 -0
  546. package/skills/plan-add/render_table.py +72 -0
  547. package/skills/plan-status/EVALS.md +78 -0
  548. package/skills/plan-status/SKILL.md +52 -0
  549. package/skills/plan-sync/EVALS.md +80 -0
  550. package/skills/plan-sync/SKILL.md +69 -0
  551. package/skills/pm-frameworks/SKILL.md +33 -0
  552. package/skills/pm-frameworks/SOURCE +39 -0
  553. package/skills/pm-frameworks/intended-vs-implemented/SKILL.md +42 -0
  554. package/skills/pm-frameworks/opportunity-solution-tree/SKILL.md +64 -0
  555. package/skills/ponytail/EVALS.md +80 -0
  556. package/skills/ponytail/SKILL.md +124 -0
  557. package/skills/product-analysis/EVALS.md +103 -0
  558. package/skills/product-analysis/SKILL.md +361 -0
  559. package/skills/research/EVALS.md +104 -0
  560. package/skills/research/SKILL.md +570 -0
  561. package/skills/rl-env/SKILL.md +53 -0
  562. package/skills/rl-env/SOURCE +41 -0
  563. package/skills/rl-env/generate-nemo-gym-env/SKILL.md +169 -0
  564. package/skills/rl-env/generate-nemo-gym-env/references/architecture.md +114 -0
  565. package/skills/rl-env/generate-openenv-env/SKILL.md +183 -0
  566. package/skills/rl-env/generate-openenv-env/references/architecture.md +82 -0
  567. package/skills/rl-env/generate-ors-env/SKILL.md +209 -0
  568. package/skills/rl-env/generate-ors-env/references/architecture.md +113 -0
  569. package/skills/rl-env/generate-verifiers-env/SKILL.md +188 -0
  570. package/skills/rl-env/generate-verifiers-env/references/architecture.md +119 -0
  571. package/skills/rl-env/rl-env-from-description/SKILL.md +124 -0
  572. package/skills/rl-env/rl-env-from-description/references/interview.md +75 -0
  573. package/skills/rl-env/rl-env-from-description/references/nemo_gym.md +41 -0
  574. package/skills/rl-env/rl-env-from-description/references/openenv.md +35 -0
  575. package/skills/rl-env/rl-env-from-description/references/ors.md +50 -0
  576. package/skills/rl-env/rl-env-from-description/references/verifiers.md +39 -0
  577. package/skills/tezgah-contract/EVALS.md +78 -0
  578. package/skills/tezgah-contract/SKILL.md +792 -0
  579. package/statusline.py +129 -0
  580. package/workflows/graph-impact.js +153 -0
  581. package/workflows/graph-map.js +158 -0
  582. 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