@r1z4x/tezgah 1.9.2 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +415 -1
- package/MANIFEST +404 -144
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.md +5 -4
- package/README.tr.md +1 -1
- package/bin/codegen +5 -2
- package/bin/tezgah-design +288 -1
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-doctor +39 -2
- package/bin/tezgah-import-ai-research +26 -12
- package/bin/tezgah-ocr-review +296 -0
- package/bin/tezgah-research +255 -44
- package/bin/tezgah-review-quote +185 -0
- package/bin/tezgah-setup +1365 -55
- package/bin/tezgah-skill +1 -1
- package/bin/tezgah-status +201 -3
- package/bin/tezgah-task +258 -20
- package/commands/adhd.md +2 -2
- package/commands/ponytail.md +4 -4
- package/docs/architecture.md +2 -2
- package/docs/citations-baseline.json +3 -3
- package/docs/clarity-baseline.json +11 -5
- package/docs/contract.md +91 -43
- package/docs/environment.md +5 -2
- package/docs/evidence.md +109 -17
- package/docs/feature-audit.md +2 -2
- package/docs/gate.md +60 -33
- package/docs/glossary.md +6 -3
- package/docs/hosts.md +313 -17
- package/docs/index.json +34 -8
- package/docs/judge.md +4 -2
- package/docs/layers.md +4 -4
- package/docs/models.md +37 -9
- package/docs/operations.md +141 -5
- package/docs/research.md +815 -8
- package/docs/skills.md +137 -13
- package/docs/status-line.md +121 -1
- package/docs/testing.md +3 -1
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +21 -0
- package/hooks/lang/tr.json +22 -1
- package/hooks/projects-stop.py +2 -1
- package/hooks/tezgah_agents.py +65 -25
- package/hooks/tezgah_apps.py +18 -0
- package/hooks/tezgah_attest.py +79 -10
- package/hooks/tezgah_components.py +4 -0
- package/hooks/tezgah_context.py +207 -55
- package/hooks/tezgah_extension.py +337 -0
- package/hooks/tezgah_gate.py +267 -79
- package/hooks/tezgah_integrity.py +864 -143
- package/hooks/tezgah_judge.py +6 -4
- package/hooks/tezgah_models.py +59 -19
- package/hooks/tezgah_orca.py +1 -1
- package/hooks/tezgah_paths.py +145 -8
- package/hooks/tezgah_policy.py +128 -57
- package/hooks/tezgah_rank.py +9 -6
- package/hooks/tezgah_replay.py +12 -6
- package/hooks/tezgah_research.py +1414 -76
- package/hooks/tezgah_research_capture.py +1544 -0
- package/hooks/tezgah_research_evidence.py +842 -0
- package/hooks/tezgah_research_stats.py +258 -0
- package/hooks/tezgah_skill_pick.py +408 -106
- package/hooks/tezgah_stack.py +236 -1
- package/hooks/tezgah_stopspec.py +8 -1
- package/hooks/tezgah_task.py +14 -5
- package/hooks/tezgah_taste_ledger.py +1 -1
- package/hosts/amp/hook.py +217 -0
- package/hosts/amp/tezgah-plugin.ts.in +386 -0
- package/hosts/antigravity/hook.py +255 -0
- package/hosts/codex/hook.py +2 -1
- package/hosts/commandcode/hook.py +239 -0
- package/hosts/copilot/hook.py +284 -0
- package/hosts/cursor/hook.py +31 -3
- package/hosts/droid/hook.py +259 -0
- package/hosts/dsh/hooks.json +1 -1
- package/hosts/gemini/hook.py +213 -0
- package/hosts/goose/hook.py +145 -0
- package/hosts/omp/hook.py +4 -1
- package/hosts/omp/tezgah-hook.ts.in +30 -7
- package/hosts/opencode/plugins/tezgah.js +309 -48
- package/hosts/opencode/tui/cli/tui.tsx +100 -0
- package/hosts/opencode/tui/tezgah-tui.tsx +2 -1
- package/hosts/qwen/hook.py +206 -0
- package/output-styles/tezgah.md +20 -15
- package/package.json +1 -1
- package/skills/ai-research/SKILL.md +21 -14
- package/skills/ai-research/SOURCE +7 -5
- package/skills/ai-research/index/1-frame.md +6 -6
- package/skills/ai-research/index/2-data.md +9 -9
- package/skills/ai-research/index/3-train.md +36 -36
- package/skills/ai-research/index/4-measure.md +13 -13
- package/skills/ai-research/index/5-run.md +11 -11
- package/skills/ai-research/index/6-write.md +23 -23
- package/skills/ai-research/library.json +104 -104
- package/skills/analyze-app/SKILL.md +17 -12
- package/skills/design-contract/SKILL.md +42 -7
- package/skills/design-contract/data-views.md +2 -2
- package/skills/design-library/EVALS.md +1 -1
- package/skills/design-library/INDEX.md +39 -39
- package/skills/design-library/SKILL.md +19 -13
- package/skills/design-library/SOURCE +45 -43
- package/skills/feature-audit/SKILL.md +20 -14
- package/skills/hallmark/LICENSE +21 -0
- package/skills/hallmark/SKILL.md +154 -0
- package/skills/hallmark/SOURCE +207 -0
- package/skills/hallmark/agents/openai.yaml +13 -0
- package/skills/hallmark/references/anatomy.md +71 -0
- package/skills/hallmark/references/brand-truth.md +40 -0
- package/skills/hallmark/references/comp.md +104 -0
- package/skills/hallmark/references/craft.md +116 -0
- package/skills/hallmark/references/dark-mode.md +49 -0
- package/skills/hallmark/references/data-viz.md +48 -0
- package/skills/hallmark/references/design-md.md +53 -0
- package/skills/hallmark/references/direction.md +52 -0
- package/skills/hallmark/references/export-formats.md +80 -0
- package/skills/hallmark/references/flow.md +131 -0
- package/skills/hallmark/references/fonts.md +58 -0
- package/skills/hallmark/references/gates.md +243 -0
- package/skills/hallmark/references/genres/atmospheric.md +24 -0
- package/skills/hallmark/references/genres/editorial.md +24 -0
- package/skills/hallmark/references/genres/modern-minimal.md +24 -0
- package/skills/hallmark/references/genres/playful.md +24 -0
- package/skills/hallmark/references/imagery.md +99 -0
- package/skills/hallmark/references/interaction-and-states.md +89 -0
- package/skills/hallmark/references/layout.md +104 -0
- package/skills/hallmark/references/macrostructures/01-bento-grid.md +21 -0
- package/skills/hallmark/references/macrostructures/02-long-document.md +35 -0
- package/skills/hallmark/references/macrostructures/03-marquee-hero.md +21 -0
- package/skills/hallmark/references/macrostructures/04-stat-led.md +21 -0
- package/skills/hallmark/references/macrostructures/05-workbench.md +21 -0
- package/skills/hallmark/references/macrostructures/06-conversational-faq.md +28 -0
- package/skills/hallmark/references/macrostructures/07-manifesto.md +28 -0
- package/skills/hallmark/references/macrostructures/08-photographic.md +21 -0
- package/skills/hallmark/references/macrostructures/09-quote-led.md +21 -0
- package/skills/hallmark/references/macrostructures/10-specimen.md +21 -0
- package/skills/hallmark/references/macrostructures/11-catalogue.md +21 -0
- package/skills/hallmark/references/macrostructures/12-letter.md +21 -0
- package/skills/hallmark/references/macrostructures/13-index-first.md +21 -0
- package/skills/hallmark/references/macrostructures/14-narrative-workflow.md +21 -0
- package/skills/hallmark/references/macrostructures/15-split-studio.md +21 -0
- package/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
- package/skills/hallmark/references/macrostructures/17-type-specimen.md +19 -0
- package/skills/hallmark/references/macrostructures/18-portfolio-grid.md +21 -0
- package/skills/hallmark/references/macrostructures/19-map-diagram.md +21 -0
- package/skills/hallmark/references/macrostructures/20-ecosystem-index.md +19 -0
- package/skills/hallmark/references/macrostructures/21-component-playground.md +21 -0
- package/skills/hallmark/references/macrostructures.md +119 -0
- package/skills/hallmark/references/microinteractions.md +80 -0
- package/skills/hallmark/references/patterns.md +202 -0
- package/skills/hallmark/references/redesign.md +88 -0
- package/skills/hallmark/references/responsive.md +46 -0
- package/skills/hallmark/references/review-finish.md +19 -0
- package/skills/hallmark/references/review-hero.md +20 -0
- package/skills/hallmark/references/scroll-choreography.md +110 -0
- package/skills/hallmark/references/texture.md +90 -0
- package/skills/hallmark/references/themes/README.md +58 -0
- package/skills/hallmark/references/themes/almanac.jpg +0 -0
- package/skills/hallmark/references/themes/almanac.md +103 -0
- package/skills/hallmark/references/themes/arcade.jpg +0 -0
- package/skills/hallmark/references/themes/arcade.md +102 -0
- package/skills/hallmark/references/themes/atelier.jpg +0 -0
- package/skills/hallmark/references/themes/atelier.md +98 -0
- package/skills/hallmark/references/themes/bloom.jpg +0 -0
- package/skills/hallmark/references/themes/bloom.md +102 -0
- package/skills/hallmark/references/themes/blueprint.jpg +0 -0
- package/skills/hallmark/references/themes/blueprint.md +103 -0
- package/skills/hallmark/references/themes/broadside.jpg +0 -0
- package/skills/hallmark/references/themes/broadside.md +108 -0
- package/skills/hallmark/references/themes/brutal.jpg +0 -0
- package/skills/hallmark/references/themes/brutal.md +95 -0
- package/skills/hallmark/references/themes/carnival.jpg +0 -0
- package/skills/hallmark/references/themes/carnival.md +104 -0
- package/skills/hallmark/references/themes/cobalt.jpg +0 -0
- package/skills/hallmark/references/themes/cobalt.md +101 -0
- package/skills/hallmark/references/themes/console.jpg +0 -0
- package/skills/hallmark/references/themes/console.md +99 -0
- package/skills/hallmark/references/themes/coral.jpg +0 -0
- package/skills/hallmark/references/themes/coral.md +102 -0
- package/skills/hallmark/references/themes/darkroom.jpg +0 -0
- package/skills/hallmark/references/themes/darkroom.md +99 -0
- package/skills/hallmark/references/themes/dose.jpg +0 -0
- package/skills/hallmark/references/themes/dose.md +98 -0
- package/skills/hallmark/references/themes/dusk.jpg +0 -0
- package/skills/hallmark/references/themes/dusk.md +105 -0
- package/skills/hallmark/references/themes/editorial.jpg +0 -0
- package/skills/hallmark/references/themes/editorial.md +97 -0
- package/skills/hallmark/references/themes/formwork.jpg +0 -0
- package/skills/hallmark/references/themes/formwork.md +107 -0
- package/skills/hallmark/references/themes/furrow.jpg +0 -0
- package/skills/hallmark/references/themes/furrow.md +98 -0
- package/skills/hallmark/references/themes/garden.jpg +0 -0
- package/skills/hallmark/references/themes/garden.md +97 -0
- package/skills/hallmark/references/themes/grid.jpg +0 -0
- package/skills/hallmark/references/themes/grid.md +101 -0
- package/skills/hallmark/references/themes/halftone.jpg +0 -0
- package/skills/hallmark/references/themes/halftone.md +106 -0
- package/skills/hallmark/references/themes/hedgerow.jpg +0 -0
- package/skills/hallmark/references/themes/hedgerow.md +104 -0
- package/skills/hallmark/references/themes/hum.jpg +0 -0
- package/skills/hallmark/references/themes/hum.md +99 -0
- package/skills/hallmark/references/themes/index.json +2191 -0
- package/skills/hallmark/references/themes/ink.jpg +0 -0
- package/skills/hallmark/references/themes/ink.md +106 -0
- package/skills/hallmark/references/themes/kiln.jpg +0 -0
- package/skills/hallmark/references/themes/kiln.md +108 -0
- package/skills/hallmark/references/themes/loom.jpg +0 -0
- package/skills/hallmark/references/themes/loom.md +106 -0
- package/skills/hallmark/references/themes/lumen.jpg +0 -0
- package/skills/hallmark/references/themes/lumen.md +103 -0
- package/skills/hallmark/references/themes/manifesto.jpg +0 -0
- package/skills/hallmark/references/themes/manifesto.md +101 -0
- package/skills/hallmark/references/themes/midnight.jpg +0 -0
- package/skills/hallmark/references/themes/midnight.md +97 -0
- package/skills/hallmark/references/themes/newsprint.jpg +0 -0
- package/skills/hallmark/references/themes/newsprint.md +102 -0
- package/skills/hallmark/references/themes/overprint.jpg +0 -0
- package/skills/hallmark/references/themes/overprint.md +101 -0
- package/skills/hallmark/references/themes/pasteup.jpg +0 -0
- package/skills/hallmark/references/themes/pasteup.md +100 -0
- package/skills/hallmark/references/themes/piste.jpg +0 -0
- package/skills/hallmark/references/themes/piste.md +99 -0
- package/skills/hallmark/references/themes/ribbon.jpg +0 -0
- package/skills/hallmark/references/themes/ribbon.md +106 -0
- package/skills/hallmark/references/themes/rim.jpg +0 -0
- package/skills/hallmark/references/themes/rim.md +96 -0
- package/skills/hallmark/references/themes/riso.jpg +0 -0
- package/skills/hallmark/references/themes/riso.md +98 -0
- package/skills/hallmark/references/themes/solstice.jpg +0 -0
- package/skills/hallmark/references/themes/solstice.md +104 -0
- package/skills/hallmark/references/themes/specimen.jpg +0 -0
- package/skills/hallmark/references/themes/specimen.md +100 -0
- package/skills/hallmark/references/themes/spore.jpg +0 -0
- package/skills/hallmark/references/themes/spore.md +104 -0
- package/skills/hallmark/references/themes/sport.jpg +0 -0
- package/skills/hallmark/references/themes/sport.md +101 -0
- package/skills/hallmark/references/themes/squeeze.jpg +0 -0
- package/skills/hallmark/references/themes/squeeze.md +99 -0
- package/skills/hallmark/references/themes/sticker.jpg +0 -0
- package/skills/hallmark/references/themes/sticker.md +106 -0
- package/skills/hallmark/references/themes/studio.jpg +0 -0
- package/skills/hallmark/references/themes/studio.md +99 -0
- package/skills/hallmark/references/themes/terminal.jpg +0 -0
- package/skills/hallmark/references/themes/terminal.md +96 -0
- package/skills/hallmark/references/themes/terrazzo.jpg +0 -0
- package/skills/hallmark/references/themes/terrazzo.md +106 -0
- package/skills/hallmark/references/themes/ultraviolet.jpg +0 -0
- package/skills/hallmark/references/themes/ultraviolet.md +106 -0
- package/skills/hallmark/references/themes/vellum.jpg +0 -0
- package/skills/hallmark/references/themes/vellum.md +105 -0
- package/skills/hallmark/references/themes/wayfind.jpg +0 -0
- package/skills/hallmark/references/themes/wayfind.md +105 -0
- package/skills/hallmark/references/themes/werkstatt.jpg +0 -0
- package/skills/hallmark/references/themes/werkstatt.md +105 -0
- package/skills/hallmark/scripts/capture.mjs +125 -0
- package/skills/hallmark/scripts/fixtures/exemplars/atmospheric.html +324 -0
- package/skills/hallmark/scripts/fixtures/exemplars/editorial.html +363 -0
- package/skills/hallmark/scripts/fixtures/exemplars/modern-minimal.html +527 -0
- package/skills/hallmark/scripts/fixtures/exemplars/playful.html +353 -0
- package/skills/hallmark/scripts/fixtures/eyebrow-fail/index.html +118 -0
- package/skills/hallmark/scripts/fixtures/eyebrow-pass/index.html +137 -0
- package/skills/hallmark/scripts/fixtures/kept/after.html +27 -0
- package/skills/hallmark/scripts/fixtures/kept/before.html +26 -0
- package/skills/hallmark/scripts/fixtures/slopped.html +145 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/Feature.vue +14 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/Hero.tsx +33 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/index.html +74 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/style.css +6 -0
- package/skills/hallmark/scripts/fixtures/tailwind-pass/Hero.tsx +33 -0
- package/skills/hallmark/scripts/fixtures/tailwind-pass/index.html +57 -0
- package/skills/hallmark/scripts/fixtures/tailwind-pass/style.css +15 -0
- package/skills/hallmark/scripts/fixtures/waiver/index.html +54 -0
- package/skills/hallmark/scripts/imagegen.mjs +85 -0
- package/skills/hallmark/scripts/install-hook.mjs +70 -0
- package/skills/hallmark/scripts/install.mjs +141 -0
- package/skills/hallmark/scripts/lint-hook.mjs +49 -0
- package/skills/hallmark/scripts/photos.json +1 -0
- package/skills/hallmark/scripts/picture.mjs +71 -0
- package/skills/hallmark/scripts/refcheck.mjs +352 -0
- package/skills/hallmark/scripts/sloplint-render.mjs +1252 -0
- package/skills/hallmark/scripts/sloplint.mjs +4422 -0
- package/skills/hallmark/scripts/theme.mjs +650 -0
- package/skills/harness/SKILL.md +17 -8
- package/skills/i-have-adhd/SKILL.md +32 -12
- package/skills/no-ai-slop/SKILL.md +15 -1
- package/skills/plan-add/SKILL.md +38 -27
- package/skills/plan-status/SKILL.md +13 -7
- package/skills/plan-sync/SKILL.md +18 -12
- package/skills/pm-frameworks/SKILL.md +19 -10
- package/skills/pm-frameworks/SOURCE +7 -5
- package/skills/ponytail/SKILL.md +19 -14
- package/skills/product-analysis/SKILL.md +22 -18
- package/skills/product-flow/SKILL.md +14 -9
- package/skills/research/SKILL.md +97 -48
- package/skills/rl-env/SKILL.md +25 -13
- package/skills/rl-env/SOURCE +8 -6
- package/skills/tezgah-contract/SKILL.md +65 -47
- package/statusline.py +20 -9
- package/workflows/graph-review.js +14 -2
- package/bin/tezgah-skill.py +0 -59
- /package/skills/ai-research/0-autoresearch-skill/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/litgpt/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/mamba/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/nanogpt/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/rwkv/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/torchtitan/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/02-tokenization/huggingface-tokenizers/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/02-tokenization/sentencepiece/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/axolotl/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/llama-factory/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/peft/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/unsloth/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/nnsight/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/pyvene/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/saelens/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/05-data-processing/nemo-curator/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/05-data-processing/ray-data/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/grpo-rl-training/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/miles/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/openrlhf/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/simpo/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/slime/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/torchforge/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/trl-fine-tuning/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/verl/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/constitutional-ai/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/llamaguard/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/nemo-guardrails/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/prompt-guard/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/accelerate/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/deepspeed/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/megatron-core/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/pytorch-fsdp2/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/pytorch-lightning/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/ray-train/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/09-infrastructure/lambda-labs/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/09-infrastructure/modal/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/09-infrastructure/skypilot/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/awq/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/bitsandbytes/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/flash-attention/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/gguf/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/gptq/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/hqq/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/ml-training-recipes/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/11-evaluation/lm-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/11-evaluation/nemo-evaluator/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/llama-cpp/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/sglang/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/tensorrt-llm/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/vllm/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/mlflow/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/swanlab/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/tensorboard/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/weights-and-biases/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/a-evolve/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/autogpt/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/crewai/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/langchain/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/llamaindex/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/chroma/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/faiss/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/pinecone/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/qdrant/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/sentence-transformers/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/dspy/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/guidance/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/instructor/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/outlines/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/17-observability/langsmith/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/17-observability/phoenix/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/audiocraft/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/blip-2/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/clip/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/cosmos-policy/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/llava/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/openpi/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/openvla-oft/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/segment-anything/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/stable-diffusion/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/whisper/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/knowledge-distillation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/long-context/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/model-merging/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/model-pruning/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/moe-training/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/speculative-decoding/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/academic-plotting/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/21-research-ideation/creative-thinking-for-research/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/22-agent-native-research-artifact/compiler/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/22-agent-native-research-artifact/research-manager/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/colour-independence/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/flexible-typography/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/information-density/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/preference-audit/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/responsive-review/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/simplified-views/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/specify/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/assess-load/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/plain-language-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/review/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/simplify/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/design-research/skills/behavioural-analytics/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/design-research/skills/qual-quant-triangulation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/audit/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/design-flow/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/feedback-and-status/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/keyboard-review/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/multi-modal-input/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/touch-target-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/voice-interaction/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-affordance/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-brand-consistency/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-color/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-composition/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-information-density/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-typography/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/pm-frameworks/intended-vs-implemented/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/pm-frameworks/opportunity-solution-tree/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-nemo-gym-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-openenv-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-ors-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-verifiers-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/rl-env-from-description/{SKILL.md → ENTRY.md} +0 -0
package/bin/tezgah-setup
CHANGED
|
@@ -166,6 +166,28 @@ DSH_WEB_PATCH = os.path.join(DSH_WEB, "cordis.patch.yml")
|
|
|
166
166
|
DSH_STATUSLINE = "tezgah-dsh-statusline"
|
|
167
167
|
OMP = tp.HOST_DIRS["omp"]
|
|
168
168
|
OMP_AGENT = os.path.join(OMP, "agent")
|
|
169
|
+
QWEN = tp.HOST_DIRS["qwen"]
|
|
170
|
+
DROID = tp.HOST_DIRS["droid"]
|
|
171
|
+
COMMANDCODE = tp.HOST_DIRS["commandcode"]
|
|
172
|
+
COPILOT = tp.HOST_DIRS["copilot"]
|
|
173
|
+
GEMINI = tp.HOST_DIRS["gemini"]
|
|
174
|
+
# tezgah's own Gemini extension: a dir Gemini loads from <home>/.gemini/extensions
|
|
175
|
+
# with no `gemini extensions install` (extension-manager.ts reads every subdir;
|
|
176
|
+
# only a non-empty `security.allowedExtensions` asks for install metadata)
|
|
177
|
+
GEMINI_EXT = os.path.join(GEMINI, "extensions", "tezgah")
|
|
178
|
+
ANTIGRAVITY = tp.HOST_DIRS["antigravity"]
|
|
179
|
+
# Antigravity's global hooks file: one top-level name per hook set, and the
|
|
180
|
+
# Antigravity App and IDE load it too (docs/hooks); tezgah owns the `tezgah` name
|
|
181
|
+
AGY_HOOKS = os.path.join(os.path.dirname(ANTIGRAVITY), "config", "hooks.json")
|
|
182
|
+
GOOSE = tp.HOST_DIRS["goose"]
|
|
183
|
+
# tezgah's own Goose plugin under ~/.agents/plugins, wholly tezgah's
|
|
184
|
+
GOOSE_PLUGIN = tp.GOOSE_PLUGIN
|
|
185
|
+
AMP = tp.HOST_DIRS["amp"]
|
|
186
|
+
# tezgah's Amp system plugin, one file Amp loads with every other .ts there
|
|
187
|
+
AMP_PLUGIN = tp.AMP_PLUGIN
|
|
188
|
+
# Amp's commit trailers, both on by default (docs cli/settings): the co-author
|
|
189
|
+
# line and the thread link; tezgah turns both off unless the user set them
|
|
190
|
+
AMP_ATTRIBUTION = ("amp.git.commit.coauthor.enabled", "amp.git.commit.ampThread.enabled")
|
|
169
191
|
OMP_MCP_SCHEMA = ("https://raw.githubusercontent.com/can1357/oh-my-pi/main/"
|
|
170
192
|
"packages/coding-agent/src/config/mcp-schema.json")
|
|
171
193
|
# App-analysis servers omp drops on its own. While its native `browser` (the eval
|
|
@@ -188,7 +210,7 @@ LOCAL_BIN = os.path.join(HOME, ".local", "bin")
|
|
|
188
210
|
SKILLS = ["harness", "tezgah-contract", "ponytail", "i-have-adhd", "no-ai-slop",
|
|
189
211
|
"analyze-app", "plan-add", "plan-status", "plan-sync", "research",
|
|
190
212
|
"product-analysis", "product-flow", "feature-audit", "design-contract",
|
|
191
|
-
"design-library", "pm-frameworks", "ai-research", "rl-env"]
|
|
213
|
+
"design-library", "hallmark", "pm-frameworks", "ai-research", "rl-env"]
|
|
192
214
|
|
|
193
215
|
|
|
194
216
|
def skills_linked(base):
|
|
@@ -209,8 +231,22 @@ RESEARCH_SKILLS = frozenset(("ai-research", "rl-env"))
|
|
|
209
231
|
# explicit value wins.
|
|
210
232
|
OPENCODE_IGNORE = [".git/**", "node_modules/**", "dist/**", "build/**",
|
|
211
233
|
".venv/**", "__pycache__/**", ".ruff_cache/**"]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def opencode_v2():
|
|
237
|
+
"""opencode 2 keeps its terminal settings in cli.json (it migrates tui.json
|
|
238
|
+
into it once, on first start) and ignores tui.json afterwards."""
|
|
239
|
+
return os.path.isfile(os.path.join(OPENCODE, "cli.json"))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# The opencode 2 status line: a link to the plugin directory hosts/opencode/tui/cli,
|
|
243
|
+
# listed in cli.json `plugins` as file://<this path>.
|
|
244
|
+
OPENCODE_CLI_PLUGIN = os.path.join(OPENCODE, "tui-plugins", "tezgah-cli")
|
|
245
|
+
|
|
246
|
+
|
|
212
247
|
WORKFLOW_FILES = ["graph-map.js", "graph-review.js", "graph-impact.js"]
|
|
213
|
-
ALL_HOSTS = ["claude", "codex", "opencode", "cursor", "dsh", "omp"
|
|
248
|
+
ALL_HOSTS = ["claude", "codex", "opencode", "cursor", "dsh", "omp", "qwen", "droid",
|
|
249
|
+
"commandcode", "copilot", "gemini", "antigravity", "goose", "amp"]
|
|
214
250
|
# orx ships an installable skill shim for four agents; dsh has no orx harness,
|
|
215
251
|
# so research routing there falls back to `orx skill` on the shell. omp has no
|
|
216
252
|
# orx target of its own, and needs none: orx's `codex` target writes its shim to
|
|
@@ -231,7 +267,8 @@ MATERIALISED = os.path.join(tp.CONFIG_DIR, "materialised")
|
|
|
231
267
|
# copies die with ~/.config/tezgah; the pre-multi-host setup wrote them to
|
|
232
268
|
# ~/.claude, which OFF_DIRS still honors, so a full uninstall sweeps those by
|
|
233
269
|
# name too - a switch left behind is a rule that stays silent with no file saying why.
|
|
234
|
-
OFF_SWITCH_NAMES = ("adhd-off", "agents-off", "consult-off", "
|
|
270
|
+
OFF_SWITCH_NAMES = ("adhd-off", "agents-off", "consult-off", "design-off", "docs-judge-off",
|
|
271
|
+
"exec-mode.off",
|
|
235
272
|
"judge-off", "lang-off", "orchestrate-off", "ponytail-auto.off",
|
|
236
273
|
"pretooluse-off", "reminder-off", "research-off", "slices-off",
|
|
237
274
|
"spec-off", "task-off", "triage-off", "update-check-off", "verify-off",
|
|
@@ -310,6 +347,73 @@ CURSOR_HOOK_EVENTS = {
|
|
|
310
347
|
"afterFileEdit": {"timeout": 10},
|
|
311
348
|
"afterAgentResponse": {"timeout": 5},
|
|
312
349
|
}
|
|
350
|
+
# Qwen Code 0.21.13: a matcher is a regex over the runtime tool id, and a
|
|
351
|
+
# command hook's `timeout` is in MILLISECONDS (60000 by default) - the seconds
|
|
352
|
+
# value the other manifests carry would time every hook out. Stop and
|
|
353
|
+
# SubagentStop run the integrity rule, which may ask a judge, so they get more.
|
|
354
|
+
# Qwen takes MCP tools as `mcp__<server>__<tool>`.
|
|
355
|
+
QWEN_PRETOOL_MATCHER = ("^(run_shell_command|monitor|write_file|edit|replace|notebook_edit|"
|
|
356
|
+
"agent|grep_search|mcp__.*)$")
|
|
357
|
+
QWEN_HOOK_EVENTS = (("SessionStart", None, 5000), ("UserPromptSubmit", None, 5000),
|
|
358
|
+
("PreToolUse", QWEN_PRETOOL_MATCHER, 5000),
|
|
359
|
+
("PostToolUse", None, 5000), ("PostToolUseFailure", None, 5000),
|
|
360
|
+
("SubagentStart", None, 5000), ("SubagentStop", None, 30000),
|
|
361
|
+
("Stop", None, 30000))
|
|
362
|
+
# Factory Droid 0.251.0 (docs.factory.com harness/hooks): a matcher is a
|
|
363
|
+
# case-sensitive regex over the tool id, and `timeout` is in seconds. Droid has
|
|
364
|
+
# no SubagentStart and no failure event. MCP tools are `mcp__<server>__<tool>`.
|
|
365
|
+
DROID_PRETOOL_MATCHER = "^(Execute|Create|Edit|ApplyPatch|Task|Grep|mcp__.*)$"
|
|
366
|
+
DROID_HOOK_EVENTS = (("SessionStart", None, 5), ("UserPromptSubmit", None, 5),
|
|
367
|
+
("PreToolUse", DROID_PRETOOL_MATCHER, 5), ("PostToolUse", None, 5),
|
|
368
|
+
("SubagentStop", None, 30), ("Stop", None, 30))
|
|
369
|
+
# Command Code (commandcode.ai/docs/hooks): four events, `timeout` in seconds, and
|
|
370
|
+
# a matcher tested against the tool's display name (SHELL, READ, WRITE, EDIT) -
|
|
371
|
+
# its settings hooks see no other tool, so MCP, subagent and web calls are not
|
|
372
|
+
# gated there. A matcher on SessionStart or Stop stops the hook from firing.
|
|
373
|
+
COMMANDCODE_PRETOOL_MATCHER = "shell|write|edit"
|
|
374
|
+
COMMANDCODE_HOOK_EVENTS = (("SessionStart", None, 5),
|
|
375
|
+
("PreToolUse", COMMANDCODE_PRETOOL_MATCHER, 5),
|
|
376
|
+
("PostToolUse", None, 5), ("Stop", None, 30))
|
|
377
|
+
# GitHub Copilot CLI (docs.github.com hooks-reference): tezgah's own file in the
|
|
378
|
+
# user hooks dir, flat entries with `timeoutSec` in seconds. The PascalCase names
|
|
379
|
+
# get snake_case payloads and Claude tool names; `subagentStart` has no PascalCase
|
|
380
|
+
# form, so its camelCase payload names no event and the command passes it. A
|
|
381
|
+
# PascalCase PreToolUse matcher is anchored and tested against the Claude tool
|
|
382
|
+
# name; an MCP tool is `<server>-<tool>`. A timed-out PreToolUse lets the call
|
|
383
|
+
# through, hence 10 s there.
|
|
384
|
+
COPILOT_PRETOOL_MATCHER = "^(?:Bash|Write|Edit|Agent|Task|Grep|.+-.+)$"
|
|
385
|
+
COPILOT_HOOK_EVENTS = (("SessionStart", None, 5), ("UserPromptSubmit", None, 5),
|
|
386
|
+
("PreToolUse", COPILOT_PRETOOL_MATCHER, 10), ("PostToolUse", None, 5),
|
|
387
|
+
("PostToolUseFailure", None, 5), ("subagentStart", None, 5),
|
|
388
|
+
("SubagentStop", None, 30), ("Stop", None, 30))
|
|
389
|
+
# Gemini CLI (geminicli hooks reference): `timeout` in MILLISECONDS, a BeforeTool
|
|
390
|
+
# matcher is a regex over the tool name, a lifecycle matcher an exact string (so
|
|
391
|
+
# none). A subagent is a tool of its own name; an MCP tool is
|
|
392
|
+
# `mcp_<server>_<tool>`. AfterAgent runs the integrity rule, which may ask a judge.
|
|
393
|
+
GEMINI_PRETOOL_MATCHER = ("^(run_shell_command|write_file|replace|grep_search|"
|
|
394
|
+
"search_file_content|codebase_investigator|generalist|"
|
|
395
|
+
"invoke_agent|mcp_.*)$")
|
|
396
|
+
GEMINI_HOOK_EVENTS = (("SessionStart", None, 5000), ("BeforeAgent", None, 5000),
|
|
397
|
+
("BeforeTool", GEMINI_PRETOOL_MATCHER, 5000),
|
|
398
|
+
("AfterTool", None, 5000), ("AfterAgent", None, 30000))
|
|
399
|
+
# Antigravity CLI (docs/hooks): `timeout` in seconds; a tool event is a matcher
|
|
400
|
+
# group (a regex over the tool name, "" for every tool), a lifecycle event a bare
|
|
401
|
+
# handler. The payload names no event, so the command does (argv[1]). An MCP
|
|
402
|
+
# tool's name is undocumented, so none is matched. Stop runs the integrity rule.
|
|
403
|
+
ANTIGRAVITY_PRETOOL_MATCHER = ("^(run_command|write_to_file|replace_file_content|"
|
|
404
|
+
"multi_replace_file_content|invoke_subagent|grep_search)$")
|
|
405
|
+
ANTIGRAVITY_HOOK_EVENTS = (("SessionStart", None, 5), ("PreInvocation", None, 5),
|
|
406
|
+
("PreToolUse", ANTIGRAVITY_PRETOOL_MATCHER, 5),
|
|
407
|
+
("PostToolUse", "", 5), ("Stop", None, 30))
|
|
408
|
+
# Goose (docs guides/context-engineering/hooks): `timeout` in seconds, a matcher a
|
|
409
|
+
# regex tested unanchored (so anchored here) against the tool name. The built-in
|
|
410
|
+
# developer tools are bare (`shell`, `write`, `edit`), an extension's tool is
|
|
411
|
+
# `<extension>__<tool>`. Only PreToolUse and Stop can block; Stop runs the
|
|
412
|
+
# integrity rule. `on_failure` is left at its default `allow` (X-D2).
|
|
413
|
+
GOOSE_PRETOOL_MATCHER = "^(shell|write|edit|.+__.+)$"
|
|
414
|
+
GOOSE_HOOK_EVENTS = (("SessionStart", None, 5), ("UserPromptSubmit", None, 5),
|
|
415
|
+
("PreToolUse", GOOSE_PRETOOL_MATCHER, 5), ("PostToolUse", None, 5),
|
|
416
|
+
("PostToolUseFailure", None, 5), ("Stop", None, 30))
|
|
313
417
|
# The feature selection for THIS run: None means "read config.json['features']",
|
|
314
418
|
# and an absent key there means the registry's declared default, which is what
|
|
315
419
|
# makes a config written before this existed behave exactly as it used to.
|
|
@@ -454,14 +558,22 @@ def hook_entries(hosts):
|
|
|
454
558
|
what re-arming would change."""
|
|
455
559
|
out = {}
|
|
456
560
|
for h in hosts:
|
|
457
|
-
if h in ("codex", "cursor"
|
|
458
|
-
|
|
561
|
+
if h in ("codex", "cursor", "qwen", "droid", "commandcode", "copilot", "gemini",
|
|
562
|
+
"antigravity", "goose"):
|
|
563
|
+
groups = {"codex": codex_hook_groups, "cursor": cursor_hook_entries,
|
|
564
|
+
"qwen": qwen_hook_groups, "droid": droid_hook_groups,
|
|
565
|
+
"commandcode": commandcode_hook_groups,
|
|
566
|
+
"copilot": copilot_hook_entries, "gemini": gemini_hook_groups,
|
|
567
|
+
"antigravity": antigravity_hook_groups,
|
|
568
|
+
"goose": goose_hook_groups}[h]()
|
|
459
569
|
out[h] = ta.own_groups({"hooks": {ev: [g] for ev, g in groups.items()}})
|
|
460
570
|
elif h == "opencode":
|
|
461
571
|
src = os.path.join(HERE, "hosts", "opencode", "plugins", "tezgah.js")
|
|
462
572
|
out[h] = {d: src for d in ta.OPENCODE_DIRS}
|
|
463
573
|
elif h == "omp":
|
|
464
574
|
out[h] = {"bridge": omp_bridge()}
|
|
575
|
+
elif h == "amp":
|
|
576
|
+
out[h] = {"plugin": ta.amp_plugin(HERE)}
|
|
465
577
|
else:
|
|
466
578
|
out[h] = ta.registration(h, HERE)[0] or {}
|
|
467
579
|
return out
|
|
@@ -665,7 +777,13 @@ def write_json(path, data):
|
|
|
665
777
|
An unchanged write is not free: it moves a host file's mtime and lays down a
|
|
666
778
|
`.tezgah-bak`, so a command whose whole point is idempotence (a repeated
|
|
667
779
|
--disable) would report a change it never made. Returns True when it wrote."""
|
|
668
|
-
|
|
780
|
+
return write_text(path, json.dumps(data, indent=2) + "\n")
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def write_text(path, text):
|
|
784
|
+
"""write_json's write for text already rendered (a JSON-with-comments file
|
|
785
|
+
edited in place): unchanged bytes skip it, a backup comes first, and the
|
|
786
|
+
file keeps its mode. Returns True when it wrote."""
|
|
669
787
|
try:
|
|
670
788
|
with open(path, encoding="utf-8") as fh:
|
|
671
789
|
if fh.read() == text:
|
|
@@ -675,8 +793,15 @@ def write_json(path, data):
|
|
|
675
793
|
mkdirp(os.path.dirname(path))
|
|
676
794
|
backup(path)
|
|
677
795
|
tmp = path + ".tmp"
|
|
678
|
-
|
|
796
|
+
existed = os.path.exists(path)
|
|
797
|
+
# over an existing file the copy is born owner-only, then takes the file's
|
|
798
|
+
# mode: a settings file holding provider keys is never readable by others,
|
|
799
|
+
# not even between the write and the mode copy
|
|
800
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600 if existed else 0o666)
|
|
801
|
+
with open(fd, "w", encoding="utf-8") as fh:
|
|
679
802
|
fh.write(text)
|
|
803
|
+
if existed:
|
|
804
|
+
shutil.copymode(path, tmp) # an owner-only config stays owner-only
|
|
680
805
|
os.replace(tmp, path)
|
|
681
806
|
return True
|
|
682
807
|
|
|
@@ -713,7 +838,8 @@ def backup_base(path):
|
|
|
713
838
|
|
|
714
839
|
def backup(path):
|
|
715
840
|
"""Copy `path` to `<backup_base>.<timestamp>.tezgah-bak` before a write
|
|
716
|
-
changes it
|
|
841
|
+
changes it, owner-only: a host config can hold provider keys (Qwen's
|
|
842
|
+
settings.json does), and a copy must not be readable where the file was not.
|
|
717
843
|
|
|
718
844
|
The suffix stays `.tezgah-bak`, so managed() and the uninstall cleanup still
|
|
719
845
|
know it; past BACKUP_CAP the middle copies go, never the oldest."""
|
|
@@ -729,6 +855,7 @@ def backup(path):
|
|
|
729
855
|
try:
|
|
730
856
|
mkdirp(os.path.dirname(dst))
|
|
731
857
|
shutil.copy2(path, dst)
|
|
858
|
+
os.chmod(dst, 0o600)
|
|
732
859
|
d, name = os.path.split(base)
|
|
733
860
|
old = sorted(entry for entry in os.listdir(d or ".")
|
|
734
861
|
if (m := BACKUP_RE.fullmatch(entry)) and m.group(1) == name)
|
|
@@ -1039,7 +1166,10 @@ def install_common(new_roots, hosts):
|
|
|
1039
1166
|
(tp.roots() or [tp.DEFAULT_ROOT])
|
|
1040
1167
|
if tp.codegraph_bin():
|
|
1041
1168
|
cfg["codegraph_bin"] = tp.codegraph_bin()
|
|
1042
|
-
|
|
1169
|
+
# merged with the record, never replaced: arming one host must not drop
|
|
1170
|
+
# the others, whose wiring stays on disk and which --upgrade reads from here
|
|
1171
|
+
recorded = cfg.get("hosts") if isinstance(cfg.get("hosts"), list) else []
|
|
1172
|
+
cfg["hosts"] = recorded + [h for h in hosts if h not in recorded]
|
|
1043
1173
|
# The resolved selection, written beside roots/hosts: the next run reads one
|
|
1044
1174
|
# persisted answer instead of the flags some earlier run happened to carry,
|
|
1045
1175
|
# which is what lets --disable mean anything. A key absent here (and in the
|
|
@@ -1081,6 +1211,15 @@ def install_common(new_roots, hosts):
|
|
|
1081
1211
|
("tezgah-render-table", os.path.join(HERE, "skills", "plan-add", "render_table.py")),
|
|
1082
1212
|
("tezgah-codex-hook", os.path.join(HERE, "hosts", "codex", "hook.py")),
|
|
1083
1213
|
("tezgah-cursor-hook", os.path.join(HERE, "hosts", "cursor", "hook.py")),
|
|
1214
|
+
("tezgah-qwen-hook", os.path.join(HERE, "hosts", "qwen", "hook.py")),
|
|
1215
|
+
("tezgah-droid-hook", os.path.join(HERE, "hosts", "droid", "hook.py")),
|
|
1216
|
+
("tezgah-commandcode-hook",
|
|
1217
|
+
os.path.join(HERE, "hosts", "commandcode", "hook.py")),
|
|
1218
|
+
("tezgah-copilot-hook", os.path.join(HERE, "hosts", "copilot", "hook.py")),
|
|
1219
|
+
("tezgah-gemini-hook", os.path.join(HERE, "hosts", "gemini", "hook.py")),
|
|
1220
|
+
("tezgah-antigravity-hook",
|
|
1221
|
+
os.path.join(HERE, "hosts", "antigravity", "hook.py")),
|
|
1222
|
+
("tezgah-goose-hook", os.path.join(HERE, "hosts", "goose", "hook.py")),
|
|
1084
1223
|
("tezgah-statusline", os.path.join(HERE, "statusline.py")),
|
|
1085
1224
|
("tezgah-triage", os.path.join(HERE, "bin", "tezgah-triage")),
|
|
1086
1225
|
("tezgah-migrate", os.path.join(HERE, "bin", "tezgah-migrate")),
|
|
@@ -1090,6 +1229,8 @@ def install_common(new_roots, hosts):
|
|
|
1090
1229
|
("tezgah-route", os.path.join(HERE, "bin", "tezgah-route")),
|
|
1091
1230
|
("tezgah-lessons", os.path.join(HERE, "bin", "tezgah-lessons")),
|
|
1092
1231
|
("tezgah-skill", os.path.join(HERE, "bin", "tezgah-skill")),
|
|
1232
|
+
("tezgah-review-quote", os.path.join(HERE, "bin", "tezgah-review-quote")),
|
|
1233
|
+
("tezgah-ocr-review", os.path.join(HERE, "bin", "tezgah-ocr-review")),
|
|
1093
1234
|
# `tezgah` is this installer under the name `tezgah update` is typed
|
|
1094
1235
|
# with (npm ships the same name). Not here on purpose: tezgah-dsh
|
|
1095
1236
|
# (wired as ~/.local/bin/dsh, see dsh_launcher) and
|
|
@@ -1594,7 +1735,7 @@ def opencode_contract():
|
|
|
1594
1735
|
def contract_artifacts(hosts):
|
|
1595
1736
|
"""{path: (fresh render, writer)} for every always-on contract artifact
|
|
1596
1737
|
these hosts read: the managed block of each static rules file (Claude,
|
|
1597
|
-
Codex, omp) and opencode's contract and skill routers."""
|
|
1738
|
+
Codex, omp, Qwen, Droid) and opencode's contract and skill routers."""
|
|
1598
1739
|
out = {}
|
|
1599
1740
|
for h in hosts:
|
|
1600
1741
|
if h in STATIC_RULES:
|
|
@@ -1647,6 +1788,11 @@ def skill_description(path):
|
|
|
1647
1788
|
else:
|
|
1648
1789
|
inline = re.search(r"description:\s*(.+)", block.group(1))
|
|
1649
1790
|
desc = inline.group(1).strip().strip("\"'") if inline else ""
|
|
1791
|
+
return _trigger_line(desc)
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def _trigger_line(desc):
|
|
1795
|
+
"""The sentence of `desc` carrying its `Use when ...` trigger, cut at 140."""
|
|
1650
1796
|
desc = re.sub(r"\s+", " ", desc).strip()
|
|
1651
1797
|
sentences = re.split(r"(?<=[.!?])\s+", desc)
|
|
1652
1798
|
line = next((s for s in sentences if re.match(r"(?:Also )?[Uu]se\b", s)),
|
|
@@ -1654,6 +1800,22 @@ def skill_description(path):
|
|
|
1654
1800
|
return line[:137].rstrip() + "..." if len(line) > 140 else line
|
|
1655
1801
|
|
|
1656
1802
|
|
|
1803
|
+
def skill_when_line(path):
|
|
1804
|
+
"""The trigger line of a SKILL.md body's `## When to use` first paragraph.
|
|
1805
|
+
|
|
1806
|
+
The frontmatter description is a short bilingual trigger list now, too short
|
|
1807
|
+
for two near-neighbours to share subject words; the long text it replaced
|
|
1808
|
+
opens the body's `## When to use`, and that is what still tells them apart.
|
|
1809
|
+
Best effort like skill_description: anything unreadable yields ""."""
|
|
1810
|
+
try:
|
|
1811
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
1812
|
+
text = fh.read()
|
|
1813
|
+
except OSError:
|
|
1814
|
+
return ""
|
|
1815
|
+
para = re.search(r"^## When to use\n\s*\n(.*?)(?:\n\s*\n|\Z)", text, re.M | re.S)
|
|
1816
|
+
return _trigger_line(para.group(1)) if para else ""
|
|
1817
|
+
|
|
1818
|
+
|
|
1657
1819
|
def skill_category(name, path=""):
|
|
1658
1820
|
if name in RESEARCH_SKILLS:
|
|
1659
1821
|
return "research & papers"
|
|
@@ -1734,12 +1896,12 @@ under out up down off again more most other some such only own same too very can
|
|
|
1734
1896
|
will just don should now read reads reading use used uses using also one two
|
|
1735
1897
|
three every each any all both new old
|
|
1736
1898
|
""".split())
|
|
1737
|
-
# Two skills a session could pick either way from:
|
|
1738
|
-
# many subject words, so the description alone
|
|
1739
|
-
# where the shipped set
|
|
1740
|
-
#
|
|
1741
|
-
#
|
|
1742
|
-
#
|
|
1899
|
+
# Two skills a session could pick either way from: the trigger lines of their
|
|
1900
|
+
# `## When to use` share this many subject words, so the description alone
|
|
1901
|
+
# cannot settle the choice. Three is where the shipped set pairs near-neighbours -
|
|
1902
|
+
# `plan-add` / `plan-status` / `plan-sync` share four or five terms,
|
|
1903
|
+
# `product-analysis` / `feature-audit` and `design-contract` / `design-library`
|
|
1904
|
+
# three - while `ponytail` / `i-have-adhd` stay apart.
|
|
1743
1905
|
COLLISION_SHARED = 3
|
|
1744
1906
|
|
|
1745
1907
|
|
|
@@ -1758,7 +1920,7 @@ def skill_collisions(groups):
|
|
|
1758
1920
|
router is what a session picks from when the descriptions tie. It reads the
|
|
1759
1921
|
shipped names only: a vendored library's internal collisions are its own
|
|
1760
1922
|
index's business, and a pair tezgah cannot rename is not a tezgah decision."""
|
|
1761
|
-
lines = {name:
|
|
1923
|
+
lines = {name: skill_when_line(path)
|
|
1762
1924
|
for cat in ROUTER_ORDER for name, path in groups[cat]
|
|
1763
1925
|
if name in SKILLS}
|
|
1764
1926
|
names = sorted(lines)
|
|
@@ -1778,7 +1940,8 @@ def _collision_section(pairs):
|
|
|
1778
1940
|
out = [
|
|
1779
1941
|
"## Collisions",
|
|
1780
1942
|
"",
|
|
1781
|
-
"Pairs a description alone cannot settle: both
|
|
1943
|
+
"Pairs a description alone cannot settle: both entries' `## When to use`"
|
|
1944
|
+
" lines carry the"
|
|
1782
1945
|
" words below, so a session could pick either way from them. Read both"
|
|
1783
1946
|
" entries before choosing.",
|
|
1784
1947
|
"",
|
|
@@ -1856,20 +2019,38 @@ def install_opencode():
|
|
|
1856
2019
|
write_artifact(SKILL_ROUTER_FULL, full, write_if_changed)
|
|
1857
2020
|
print("%s %s (always-on, %d chars)" % (OK, SKILL_ROUTER, len(always)))
|
|
1858
2021
|
print("%s %s (on demand, %d chars)" % (OK, SKILL_ROUTER_FULL, len(full)))
|
|
1859
|
-
# TUI status line: opencode has no command statusLine, only a TUI plugin
|
|
1860
|
-
#
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2022
|
+
# TUI status line: opencode has no command statusLine, only a TUI plugin.
|
|
2023
|
+
# opencode 1 declares it in tui.json (a local spec resolves relative to it);
|
|
2024
|
+
# opencode 2 ignores tui.json and lists CLI plugins in cli.json `plugins`. A
|
|
2025
|
+
# local v2 entry must name a DIRECTORY whose `tui` module default-exports
|
|
2026
|
+
# Plugin.define({id, setup}) - a spec naming a file is skipped (measured on
|
|
2027
|
+
# v2.0.32) - so the directory hosts/opencode/tui/cli is what gets linked.
|
|
2028
|
+
v2 = opencode_v2()
|
|
2029
|
+
if v2:
|
|
2030
|
+
dst = OPENCODE_CLI_PLUGIN
|
|
2031
|
+
print(link(os.path.join(HERE, "hosts", "opencode", "tui", "cli"), dst))
|
|
2032
|
+
cpath = os.path.join(OPENCODE, "cli.json")
|
|
2033
|
+
cli = read_config(cpath, {})
|
|
2034
|
+
if cli is not None:
|
|
2035
|
+
plugins = cli.get("plugins") or []
|
|
2036
|
+
if "file://" + dst not in plugins:
|
|
2037
|
+
plugins.append("file://" + dst)
|
|
2038
|
+
cli["plugins"] = plugins
|
|
2039
|
+
write_json(cpath, cli)
|
|
2040
|
+
print("%s cli.json (status line plugin)" % OK)
|
|
2041
|
+
else:
|
|
2042
|
+
print(link(os.path.join(HERE, "hosts", "opencode", "tui", "tezgah-tui.tsx"),
|
|
2043
|
+
os.path.join(OPENCODE, "tui-plugins", "tezgah-tui.tsx")))
|
|
2044
|
+
tpath = os.path.join(OPENCODE, "tui.json")
|
|
2045
|
+
tui = read_config(tpath, {"$schema": "https://opencode.ai/tui.json"})
|
|
2046
|
+
if tui is not None:
|
|
2047
|
+
plugins = tui.get("plugin") or []
|
|
2048
|
+
spec = "./tui-plugins/tezgah-tui.tsx"
|
|
2049
|
+
if spec not in [p if isinstance(p, str) else p[0] for p in plugins]:
|
|
2050
|
+
plugins.append(spec)
|
|
2051
|
+
tui["plugin"] = plugins
|
|
2052
|
+
write_json(tpath, tui)
|
|
2053
|
+
print("%s tui.json (status line plugin)" % OK)
|
|
1873
2054
|
cfg = read_config(os.path.join(OPENCODE, "opencode.json"),
|
|
1874
2055
|
{"$schema": "https://opencode.ai/config.json"})
|
|
1875
2056
|
if cfg is None:
|
|
@@ -1921,7 +2102,8 @@ def install_opencode():
|
|
|
1921
2102
|
# Context hygiene: prune replaces old tool results in the prompt with
|
|
1922
2103
|
# "[Old tool result content cleared]" so a long session stops re-sending
|
|
1923
2104
|
# every output on every step. setdefault keeps a user's explicit choice.
|
|
1924
|
-
|
|
2105
|
+
if not v2: # opencode 2 omits it as an unsupported legacy setting
|
|
2106
|
+
cfg.setdefault("compaction", {}).setdefault("prune", True)
|
|
1925
2107
|
wat = cfg.setdefault("watcher", {})
|
|
1926
2108
|
ign = wat.setdefault("ignore", [])
|
|
1927
2109
|
for pat in OPENCODE_IGNORE:
|
|
@@ -1929,7 +2111,8 @@ def install_opencode():
|
|
|
1929
2111
|
ign.append(pat)
|
|
1930
2112
|
changed = write_json(os.path.join(OPENCODE, "opencode.json"), cfg)
|
|
1931
2113
|
print("%s opencode.json (instructions + skill router + mcp + skill deny)" % OK)
|
|
1932
|
-
print("%s opencode.json (
|
|
2114
|
+
print("%s opencode.json (%swatcher.ignore)"
|
|
2115
|
+
% (OK, "" if v2 else "compaction.prune + "))
|
|
1933
2116
|
if changed or dropped:
|
|
1934
2117
|
restart_hint("opencode", os.path.join(OPENCODE, "opencode.json"))
|
|
1935
2118
|
|
|
@@ -2026,6 +2209,470 @@ def install_cursor():
|
|
|
2026
2209
|
restart_hint("cursor", mpath)
|
|
2027
2210
|
|
|
2028
2211
|
|
|
2212
|
+
def qwen_hook_groups():
|
|
2213
|
+
"""{event: the group install_qwen writes}; `hook_entries` plans from it."""
|
|
2214
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-qwen-hook"))
|
|
2215
|
+
out = {}
|
|
2216
|
+
for ev, matcher, timeout in QWEN_HOOK_EVENTS:
|
|
2217
|
+
group = {"matcher": matcher} if matcher else {}
|
|
2218
|
+
group["hooks"] = [{"type": "command", "command": cmd, "timeout": timeout}]
|
|
2219
|
+
out[ev] = group
|
|
2220
|
+
return out
|
|
2221
|
+
|
|
2222
|
+
|
|
2223
|
+
def install_qwen():
|
|
2224
|
+
"""Qwen Code: hook groups and the status line in `settings.json`, skill
|
|
2225
|
+
links, and the always-on block in `QWEN.md`. settings.json is shared - Orca
|
|
2226
|
+
writes its own hooks there and the user keeps provider keys in it - so only a
|
|
2227
|
+
group naming tezgah is replaced, the test uninstall drops by, and the backup
|
|
2228
|
+
`write_json` lays down is owner-only (`backup`)."""
|
|
2229
|
+
print("\nqwen:")
|
|
2230
|
+
path = os.path.join(QWEN, "settings.json")
|
|
2231
|
+
data = read_config(path, {})
|
|
2232
|
+
if data is not None:
|
|
2233
|
+
hooks = data.setdefault("hooks", {})
|
|
2234
|
+
for ev, group in qwen_hook_groups().items():
|
|
2235
|
+
arr = hooks.setdefault(ev, [])
|
|
2236
|
+
arr[:] = [e for e in arr if "tezgah" not in json.dumps(e)]
|
|
2237
|
+
arr.append(group)
|
|
2238
|
+
# Qwen's command status line: the session JSON on stdin, ANSI kept
|
|
2239
|
+
data.setdefault("ui", {})["statusLine"] = {
|
|
2240
|
+
"type": "command",
|
|
2241
|
+
"command": tp.hook_command(
|
|
2242
|
+
os.path.join(tp.BIN_DIR, "tezgah-statusline")) + " --qwen",
|
|
2243
|
+
"respectUserColors": True,
|
|
2244
|
+
}
|
|
2245
|
+
write_json(path, data)
|
|
2246
|
+
print("%s %s (hooks + ui.statusLine)" % (OK, path))
|
|
2247
|
+
for s in SKILLS:
|
|
2248
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(QWEN, "skills", s)))
|
|
2249
|
+
print("%s %s (always-on contract)" % (OK, write_rules("qwen")))
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
def droid_hook_groups():
|
|
2253
|
+
"""{event: the group install_droid writes}; `hook_entries` plans from it."""
|
|
2254
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-droid-hook"))
|
|
2255
|
+
out = {}
|
|
2256
|
+
for ev, matcher, timeout in DROID_HOOK_EVENTS:
|
|
2257
|
+
group = {"matcher": matcher} if matcher else {}
|
|
2258
|
+
group["hooks"] = [{"type": "command", "command": cmd, "timeout": timeout}]
|
|
2259
|
+
out[ev] = group
|
|
2260
|
+
return out
|
|
2261
|
+
|
|
2262
|
+
|
|
2263
|
+
# a JSON string, kept, or a `//` / `/* */` comment, dropped
|
|
2264
|
+
JSONC_TOKENS = re.compile(r'("(?:\\.|[^"\\])*")|//[^\n]*|/\*.*?\*/', re.S)
|
|
2265
|
+
|
|
2266
|
+
|
|
2267
|
+
def read_jsonc(path):
|
|
2268
|
+
"""The object in a JSON-with-comments file (Droid's settings.json), {} when
|
|
2269
|
+
the file is absent, None when it does not parse. A whole rewrite would drop
|
|
2270
|
+
the user's comments, so the only writers are `jsonc_put`/`jsonc_drop`.
|
|
2271
|
+
ponytail: a trailing comma inside a string value is dropped too; the readers
|
|
2272
|
+
only ask for top-level keys."""
|
|
2273
|
+
try:
|
|
2274
|
+
with open(path, encoding="utf-8") as fh:
|
|
2275
|
+
text = fh.read()
|
|
2276
|
+
except FileNotFoundError:
|
|
2277
|
+
return {}
|
|
2278
|
+
except OSError:
|
|
2279
|
+
return None
|
|
2280
|
+
return jsonc_loads(text)
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
def droid_hooks_blocked():
|
|
2284
|
+
"""Why tezgah must not create ~/.factory/hooks.json, or None. Droid reads the
|
|
2285
|
+
`hooks` key of settings.json only while hooks.json is absent, so creating one
|
|
2286
|
+
beside such a key would switch the user's own hooks off."""
|
|
2287
|
+
if os.path.exists(os.path.join(DROID, "hooks.json")):
|
|
2288
|
+
return None
|
|
2289
|
+
path = os.path.join(DROID, "settings.json")
|
|
2290
|
+
data = read_jsonc(path)
|
|
2291
|
+
if data is None:
|
|
2292
|
+
return "%s does not parse, so its `hooks` key cannot be ruled out" % path
|
|
2293
|
+
if "hooks" in data:
|
|
2294
|
+
return ("%s carries a `hooks` key, which Droid ignores once hooks.json "
|
|
2295
|
+
"exists - move those hooks into hooks.json, then re-run --install" % path)
|
|
2296
|
+
return None
|
|
2297
|
+
|
|
2298
|
+
|
|
2299
|
+
def droid_statusline():
|
|
2300
|
+
return tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-statusline")) + " --droid"
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
def jsonc_loads(text):
|
|
2304
|
+
"""read_jsonc's parse of text already read: the object, or None."""
|
|
2305
|
+
text = re.sub(r",(\s*[}\]])", r"\1", JSONC_TOKENS.sub(lambda m: m.group(1) or "", text))
|
|
2306
|
+
try:
|
|
2307
|
+
data = json.loads(text)
|
|
2308
|
+
except ValueError:
|
|
2309
|
+
return None
|
|
2310
|
+
return data if isinstance(data, dict) else None
|
|
2311
|
+
|
|
2312
|
+
|
|
2313
|
+
# the object's opening brace, past any comment or string before it
|
|
2314
|
+
JSONC_OPEN = re.compile(r'("(?:\\.|[^"\\])*")|//[^\n]*|/\*.*?\*/|(\{)', re.S)
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
def jsonc_put(text, key, value):
|
|
2318
|
+
"""`text` (JSON with comments) with the top-level `key` set to `value` and
|
|
2319
|
+
every comment kept, or None when that cannot be done safely: the file does
|
|
2320
|
+
not parse, the key holds an object or list, or the edit would change
|
|
2321
|
+
anything else. An absent key goes in as one line under the opening brace,
|
|
2322
|
+
the shape `jsonc_drop` takes back out."""
|
|
2323
|
+
data = jsonc_loads(text)
|
|
2324
|
+
if data is None:
|
|
2325
|
+
return None
|
|
2326
|
+
want = dict(data, **{key: value})
|
|
2327
|
+
if key in data:
|
|
2328
|
+
if data[key] == value:
|
|
2329
|
+
return text
|
|
2330
|
+
if isinstance(data[key], (dict, list)):
|
|
2331
|
+
return None
|
|
2332
|
+
new = re.sub(r'("%s"\s*:\s*)%s' % (re.escape(key), re.escape(json.dumps(data[key]))),
|
|
2333
|
+
lambda m: m.group(1) + json.dumps(value), text, count=1)
|
|
2334
|
+
else:
|
|
2335
|
+
brace = next((m for m in JSONC_OPEN.finditer(text) if m.group(2)), None)
|
|
2336
|
+
if brace is None:
|
|
2337
|
+
return None
|
|
2338
|
+
line = "\n %s: %s%s" % (json.dumps(key), json.dumps(value), "," if data else "")
|
|
2339
|
+
new = text[:brace.end()] + line + text[brace.end():]
|
|
2340
|
+
return new if jsonc_loads(new) == want else None
|
|
2341
|
+
|
|
2342
|
+
|
|
2343
|
+
def jsonc_drop(text, key, value):
|
|
2344
|
+
"""`text` without the one-line `key: value` member `jsonc_put` inserted, or
|
|
2345
|
+
None when that exact line is not there or its removal changes anything else."""
|
|
2346
|
+
data = jsonc_loads(text)
|
|
2347
|
+
if data is None or data.get(key) != value:
|
|
2348
|
+
return None
|
|
2349
|
+
new = re.sub(r'\n[ \t]*%s: %s,?' % (re.escape(json.dumps(key)), re.escape(json.dumps(value))),
|
|
2350
|
+
"", text, count=1)
|
|
2351
|
+
# the line's own comma may now sit before the closing brace (comments may
|
|
2352
|
+
# follow it): take it out, unless the file already carried one there
|
|
2353
|
+
tail = r",(\s*}(?:\s|//[^\n]*|/\*.*?\*/)*)$"
|
|
2354
|
+
if not re.search(tail, text, re.S):
|
|
2355
|
+
new = re.sub(tail, r"\1", new, flags=re.S)
|
|
2356
|
+
rest = dict(data)
|
|
2357
|
+
rest.pop(key)
|
|
2358
|
+
return new if jsonc_loads(new) == rest else None
|
|
2359
|
+
|
|
2360
|
+
|
|
2361
|
+
def install_droid():
|
|
2362
|
+
"""Factory Droid: hook groups in `~/.factory/hooks.json`, the status line in
|
|
2363
|
+
`settings.json`, skill links, and the always-on block in `AGENTS.md`.
|
|
2364
|
+
hooks.json may hold the user's own groups, so only a group naming tezgah is
|
|
2365
|
+
replaced. It is not created while settings.json declares hooks
|
|
2366
|
+
(`droid_hooks_blocked`). settings.json is JSON with comments, so the status
|
|
2367
|
+
line and `includeCoAuthoredByDroid: false` (no co-author trailer: the
|
|
2368
|
+
no-attribution rule) are edited in place with every comment kept
|
|
2369
|
+
(`jsonc_put`); a file that cannot be edited safely is left alone and named."""
|
|
2370
|
+
print("\ndroid:")
|
|
2371
|
+
path = os.path.join(DROID, "hooks.json")
|
|
2372
|
+
why = droid_hooks_blocked()
|
|
2373
|
+
if why:
|
|
2374
|
+
print("%s %s not written: %s" % (NO, path, why))
|
|
2375
|
+
else:
|
|
2376
|
+
data = read_config(path, {})
|
|
2377
|
+
if data is not None:
|
|
2378
|
+
for ev, group in droid_hook_groups().items():
|
|
2379
|
+
arr = data.setdefault(ev, [])
|
|
2380
|
+
arr[:] = [e for e in arr if "tezgah" not in json.dumps(e)]
|
|
2381
|
+
arr.append(group)
|
|
2382
|
+
write_json(path, data)
|
|
2383
|
+
print("%s %s (hooks)" % (OK, path))
|
|
2384
|
+
spath = os.path.join(DROID, "settings.json")
|
|
2385
|
+
try:
|
|
2386
|
+
with open(spath, encoding="utf-8") as fh:
|
|
2387
|
+
text = fh.read()
|
|
2388
|
+
except FileNotFoundError:
|
|
2389
|
+
text = "{}\n"
|
|
2390
|
+
except OSError:
|
|
2391
|
+
text = None
|
|
2392
|
+
# the command's stdout is drawn above the input (settings reference)
|
|
2393
|
+
line = {"command": droid_statusline()}
|
|
2394
|
+
try:
|
|
2395
|
+
plain = json.loads(text) if text is not None else None
|
|
2396
|
+
except ValueError:
|
|
2397
|
+
plain = None # comments: edit in place
|
|
2398
|
+
if why:
|
|
2399
|
+
pass # the install stopped at the user's own `hooks` key: settings stays as it is
|
|
2400
|
+
elif isinstance(plain, dict):
|
|
2401
|
+
# plain JSON (an earlier install wrote it): set both keys and rewrite, as before
|
|
2402
|
+
ours = "tezgah-statusline" in json.dumps(plain.get("statusLine") or "")
|
|
2403
|
+
if ours or "statusLine" not in plain:
|
|
2404
|
+
plain["statusLine"] = line
|
|
2405
|
+
plain["includeCoAuthoredByDroid"] = False
|
|
2406
|
+
write_json(spath, plain)
|
|
2407
|
+
print("%s %s (statusLine%s, includeCoAuthoredByDroid false)" % (
|
|
2408
|
+
OK, spath, "" if plain["statusLine"] == line else " left as the user set it"))
|
|
2409
|
+
elif text is not None:
|
|
2410
|
+
old = (jsonc_loads(text) or {}).get("statusLine")
|
|
2411
|
+
stale = old != line and "tezgah-statusline" in json.dumps(old or "")
|
|
2412
|
+
if stale:
|
|
2413
|
+
dropped = jsonc_drop(text, "statusLine", old) # an earlier install's line
|
|
2414
|
+
text, stale = (dropped, False) if dropped is not None else (text, True)
|
|
2415
|
+
# each key on its own: a user's own statusLine never blocks the trailer switch
|
|
2416
|
+
new = jsonc_put(text, "statusLine", line) or text
|
|
2417
|
+
new = jsonc_put(new, "includeCoAuthoredByDroid", False) or new
|
|
2418
|
+
if new != text:
|
|
2419
|
+
write_text(spath, new)
|
|
2420
|
+
done = jsonc_loads(new) or {}
|
|
2421
|
+
print("%s %s (statusLine%s, includeCoAuthoredByDroid %s)" % (
|
|
2422
|
+
OK if done.get("includeCoAuthoredByDroid") is False and not stale else NO, spath,
|
|
2423
|
+
"" if done.get("statusLine") == line else
|
|
2424
|
+
" from an earlier install could not be refreshed: set it with /statusline to `%s`"
|
|
2425
|
+
% line["command"] if stale else " left as the user set it",
|
|
2426
|
+
"false" if done.get("includeCoAuthoredByDroid") is False else "not set: edit by hand"))
|
|
2427
|
+
else:
|
|
2428
|
+
print("%s %s cannot be read: set the status line with /statusline to `%s` and "
|
|
2429
|
+
"`includeCoAuthoredByDroid` to false" % (NO, spath, droid_statusline()))
|
|
2430
|
+
for s in SKILLS:
|
|
2431
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(DROID, "skills", s)))
|
|
2432
|
+
print("%s %s (always-on contract)" % (OK, write_rules("droid")))
|
|
2433
|
+
|
|
2434
|
+
|
|
2435
|
+
def commandcode_hook_groups():
|
|
2436
|
+
"""{event: the group install_commandcode writes}; `hook_entries` plans from it."""
|
|
2437
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-commandcode-hook"))
|
|
2438
|
+
out = {}
|
|
2439
|
+
for ev, matcher, timeout in COMMANDCODE_HOOK_EVENTS:
|
|
2440
|
+
group = {"matcher": matcher} if matcher else {}
|
|
2441
|
+
group["hooks"] = [{"type": "command", "command": cmd, "timeout": timeout}]
|
|
2442
|
+
out[ev] = group
|
|
2443
|
+
return out
|
|
2444
|
+
|
|
2445
|
+
|
|
2446
|
+
def install_commandcode():
|
|
2447
|
+
"""Command Code: hook groups in the `hooks` key of
|
|
2448
|
+
`~/.commandcode/settings.json`, skill links, and the always-on block in
|
|
2449
|
+
`AGENTS.md`. settings.json is the user's file, so only a group naming tezgah
|
|
2450
|
+
is replaced and the backup is owner-only (`backup`). The same write turns
|
|
2451
|
+
off the default `Co-authored-by: CommandCodeBot` commit trailer
|
|
2452
|
+
(`attribution.commit` "", docs/settings) unless the user set a value."""
|
|
2453
|
+
print("\ncommandcode:")
|
|
2454
|
+
path = os.path.join(COMMANDCODE, "settings.json")
|
|
2455
|
+
data = read_config(path, {})
|
|
2456
|
+
if data is not None:
|
|
2457
|
+
hooks = data.setdefault("hooks", {})
|
|
2458
|
+
for ev, group in commandcode_hook_groups().items():
|
|
2459
|
+
arr = hooks.setdefault(ev, [])
|
|
2460
|
+
arr[:] = [e for e in arr if "tezgah" not in json.dumps(e)]
|
|
2461
|
+
arr.append(group)
|
|
2462
|
+
attr = data.setdefault("attribution", {})
|
|
2463
|
+
if isinstance(attr, dict) and "commit" not in attr:
|
|
2464
|
+
attr["commit"] = ""
|
|
2465
|
+
write_json(path, data)
|
|
2466
|
+
print("%s %s (hooks + attribution.commit)" % (OK, path))
|
|
2467
|
+
if not isinstance(attr, dict) or attr.get("commit") != "":
|
|
2468
|
+
print("%s %s keeps your attribution.commit, so commits still carry a "
|
|
2469
|
+
"co-author trailer" % (NO, path))
|
|
2470
|
+
for s in SKILLS:
|
|
2471
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(COMMANDCODE, "skills", s)))
|
|
2472
|
+
print("%s %s (always-on contract)" % (OK, write_rules("commandcode")))
|
|
2473
|
+
|
|
2474
|
+
|
|
2475
|
+
def copilot_hook_entries():
|
|
2476
|
+
"""{event: the entry install_copilot writes}; `hook_entries` plans from it."""
|
|
2477
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-copilot-hook"))
|
|
2478
|
+
out = {}
|
|
2479
|
+
for ev, matcher, timeout in COPILOT_HOOK_EVENTS:
|
|
2480
|
+
entry = {"type": "command", "timeoutSec": timeout,
|
|
2481
|
+
# a camelCase payload names no event, so the command does
|
|
2482
|
+
"command": cmd + " " + ev if ev[0].islower() else cmd}
|
|
2483
|
+
if matcher:
|
|
2484
|
+
entry["matcher"] = matcher
|
|
2485
|
+
out[ev] = entry
|
|
2486
|
+
return out
|
|
2487
|
+
|
|
2488
|
+
|
|
2489
|
+
def copilot_statusline():
|
|
2490
|
+
return tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-statusline")) + " --copilot"
|
|
2491
|
+
|
|
2492
|
+
|
|
2493
|
+
def install_copilot():
|
|
2494
|
+
"""GitHub Copilot CLI: `$COPILOT_HOME/hooks/tezgah.json`, a hooks file wholly
|
|
2495
|
+
tezgah's (the inline `hooks` key of settings.json is never used: one bad item
|
|
2496
|
+
there rejects every hook in it), the status line and `includeCoAuthoredBy`
|
|
2497
|
+
in settings.json, skill links, and the always-on block in
|
|
2498
|
+
`copilot-instructions.md`. settings.json is JSON with comments, so it is
|
|
2499
|
+
written only while it parses as plain JSON; `includeCoAuthoredBy` (default
|
|
2500
|
+
true, a `Co-authored-by` trailer on the agent's commits) is set false unless
|
|
2501
|
+
the user set it."""
|
|
2502
|
+
print("\ncopilot:")
|
|
2503
|
+
path = os.path.join(COPILOT, "hooks", "tezgah.json")
|
|
2504
|
+
write_json(path, {"version": 1, "hooks": {ev: [e] for ev, e in
|
|
2505
|
+
copilot_hook_entries().items()}})
|
|
2506
|
+
print("%s %s (hooks)" % (OK, path))
|
|
2507
|
+
spath = os.path.join(COPILOT, "settings.json")
|
|
2508
|
+
settings = read_json(spath, None) if os.path.exists(spath) else {}
|
|
2509
|
+
if isinstance(settings, dict):
|
|
2510
|
+
settings["statusLine"] = {"type": "command", "command": copilot_statusline()}
|
|
2511
|
+
settings.setdefault("includeCoAuthoredBy", False)
|
|
2512
|
+
write_json(spath, settings)
|
|
2513
|
+
print("%s %s (statusLine + includeCoAuthoredBy)" % (OK, spath))
|
|
2514
|
+
if settings["includeCoAuthoredBy"] is not False:
|
|
2515
|
+
print("%s %s keeps your includeCoAuthoredBy, so commits still carry a "
|
|
2516
|
+
"co-author trailer" % (NO, spath))
|
|
2517
|
+
else:
|
|
2518
|
+
print("%s %s holds comments, which a rewrite would drop: set `statusLine` to "
|
|
2519
|
+
"`%s` and `includeCoAuthoredBy` to false there" % (NO, spath,
|
|
2520
|
+
copilot_statusline()))
|
|
2521
|
+
for s in SKILLS:
|
|
2522
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(COPILOT, "skills", s)))
|
|
2523
|
+
print("%s %s (always-on contract)" % (OK, write_rules("copilot")))
|
|
2524
|
+
|
|
2525
|
+
|
|
2526
|
+
def gemini_hook_groups():
|
|
2527
|
+
"""{event: the group install_gemini writes}; `hook_entries` plans from it."""
|
|
2528
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-gemini-hook"))
|
|
2529
|
+
out = {}
|
|
2530
|
+
for ev, matcher, timeout in GEMINI_HOOK_EVENTS:
|
|
2531
|
+
group = {"matcher": matcher} if matcher else {}
|
|
2532
|
+
group["hooks"] = [{"type": "command", "command": cmd, "timeout": timeout}]
|
|
2533
|
+
out[ev] = group
|
|
2534
|
+
return out
|
|
2535
|
+
|
|
2536
|
+
|
|
2537
|
+
def install_gemini():
|
|
2538
|
+
"""Gemini CLI: tezgah's own extension dir, `gemini-extension.json` beside
|
|
2539
|
+
`hooks/hooks.json` and `skills/` links, wholly tezgah's; and the always-on
|
|
2540
|
+
block in `GEMINI.md`. settings.json is never written: it is JSON with
|
|
2541
|
+
comments and holds the user's keys. Gemini's default prompt adds no
|
|
2542
|
+
co-author trailer and no setting names one, so there is nothing to turn off."""
|
|
2543
|
+
print("\ngemini:")
|
|
2544
|
+
write_json(os.path.join(GEMINI_EXT, "gemini-extension.json"),
|
|
2545
|
+
{"name": "tezgah", "version": plugin_version(),
|
|
2546
|
+
"description": "tezgah lifecycle hooks and skills"})
|
|
2547
|
+
path = os.path.join(GEMINI_EXT, "hooks", "hooks.json")
|
|
2548
|
+
write_json(path, {"hooks": {ev: [g] for ev, g in gemini_hook_groups().items()}})
|
|
2549
|
+
print("%s %s (extension hooks)" % (OK, path))
|
|
2550
|
+
for s in SKILLS:
|
|
2551
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(GEMINI_EXT, "skills", s)))
|
|
2552
|
+
print("%s %s (always-on contract)" % (OK, write_rules("gemini")))
|
|
2553
|
+
|
|
2554
|
+
|
|
2555
|
+
def antigravity_hook_groups():
|
|
2556
|
+
"""{event: the item install_antigravity writes under hooks.json's `tezgah`
|
|
2557
|
+
name}: a matcher group for a tool event, a bare handler for the others
|
|
2558
|
+
(docs/hooks "Schema and file format"); `hook_entries` plans from it."""
|
|
2559
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-antigravity-hook"))
|
|
2560
|
+
out = {}
|
|
2561
|
+
for ev, matcher, timeout in ANTIGRAVITY_HOOK_EVENTS:
|
|
2562
|
+
handler = {"type": "command", "command": cmd + " " + ev, "timeout": timeout}
|
|
2563
|
+
out[ev] = handler if matcher is None else {"matcher": matcher, "hooks": [handler]}
|
|
2564
|
+
return out
|
|
2565
|
+
|
|
2566
|
+
|
|
2567
|
+
def antigravity_statusline():
|
|
2568
|
+
return tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-statusline")) + " --antigravity"
|
|
2569
|
+
|
|
2570
|
+
|
|
2571
|
+
def install_antigravity():
|
|
2572
|
+
"""Antigravity CLI: tezgah's hook set under the top-level `tezgah` name of
|
|
2573
|
+
`~/.gemini/config/hooks.json`, beside the user's other names (hooks in
|
|
2574
|
+
antigravity-cli/settings.json are never loaded, docs/hooks); the Antigravity
|
|
2575
|
+
App and IDE read that file too. The status line in
|
|
2576
|
+
`~/.gemini/antigravity-cli/settings.json`, written only while it is plain
|
|
2577
|
+
JSON; skill links in `antigravity-cli/skills`; the always-on block in the
|
|
2578
|
+
`~/.gemini/GEMINI.md` Gemini CLI shares. Antigravity adds no co-author
|
|
2579
|
+
trailer of its own and no setting names one, so there is nothing to turn off."""
|
|
2580
|
+
print("\nantigravity:")
|
|
2581
|
+
data = read_config(AGY_HOOKS, {})
|
|
2582
|
+
if data is not None:
|
|
2583
|
+
data["tezgah"] = {ev: [g] for ev, g in antigravity_hook_groups().items()}
|
|
2584
|
+
write_json(AGY_HOOKS, data)
|
|
2585
|
+
print("%s %s (hooks; the Antigravity App and IDE load them too)" % (OK, AGY_HOOKS))
|
|
2586
|
+
spath = os.path.join(ANTIGRAVITY, "settings.json")
|
|
2587
|
+
settings = read_json(spath, None) if os.path.exists(spath) else {}
|
|
2588
|
+
if isinstance(settings, dict):
|
|
2589
|
+
settings["statusLine"] = {"type": "command", "command": antigravity_statusline()}
|
|
2590
|
+
write_json(spath, settings)
|
|
2591
|
+
print("%s %s (statusLine)" % (OK, spath))
|
|
2592
|
+
else:
|
|
2593
|
+
print("%s %s is not plain JSON, which a rewrite would lose: set `statusLine` "
|
|
2594
|
+
"to {\"type\": \"command\", \"command\": \"%s\"} there"
|
|
2595
|
+
% (NO, spath, antigravity_statusline()))
|
|
2596
|
+
for s in SKILLS:
|
|
2597
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(ANTIGRAVITY, "skills", s)))
|
|
2598
|
+
print("%s %s (always-on contract)" % (OK, write_rules("antigravity")))
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
def goose_hook_groups():
|
|
2602
|
+
"""{event: the group install_goose writes}; `hook_entries` plans from it."""
|
|
2603
|
+
cmd = tp.hook_command(os.path.join(tp.BIN_DIR, "tezgah-goose-hook"))
|
|
2604
|
+
out = {}
|
|
2605
|
+
for ev, matcher, timeout in GOOSE_HOOK_EVENTS:
|
|
2606
|
+
group = {"matcher": matcher} if matcher else {}
|
|
2607
|
+
group["hooks"] = [{"type": "command", "command": cmd, "timeout": timeout}]
|
|
2608
|
+
out[ev] = group
|
|
2609
|
+
return out
|
|
2610
|
+
|
|
2611
|
+
|
|
2612
|
+
def install_goose():
|
|
2613
|
+
"""Goose: tezgah's own plugin dir, `~/.agents/plugins/tezgah/`, wholly
|
|
2614
|
+
tezgah's - `plugin.json` beside `hooks/hooks.json` and `skills/` links (Goose
|
|
2615
|
+
loads them as `tezgah:<skill>`) - and the always-on block in
|
|
2616
|
+
`~/.config/goose/.goosehints`. The user's other `~/.agents` content is never
|
|
2617
|
+
touched. `on_failure` stays at its default `allow`, so a hook fault lets the
|
|
2618
|
+
call through. Neither Goose's docs nor its system prompt add a co-author
|
|
2619
|
+
trailer or name a setting for one, so there is nothing to turn off."""
|
|
2620
|
+
print("\ngoose:")
|
|
2621
|
+
write_json(os.path.join(GOOSE_PLUGIN, "plugin.json"),
|
|
2622
|
+
{"name": "tezgah", "version": plugin_version(),
|
|
2623
|
+
"description": "tezgah lifecycle hooks and skills"})
|
|
2624
|
+
path = os.path.join(GOOSE_PLUGIN, "hooks", "hooks.json")
|
|
2625
|
+
write_json(path, {"hooks": {ev: [g] for ev, g in goose_hook_groups().items()}})
|
|
2626
|
+
print("%s %s (plugin hooks)" % (OK, path))
|
|
2627
|
+
for s in SKILLS:
|
|
2628
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(GOOSE_PLUGIN, "skills", s)))
|
|
2629
|
+
print("%s %s (always-on contract)" % (OK, write_rules("goose")))
|
|
2630
|
+
|
|
2631
|
+
|
|
2632
|
+
def amp_settings():
|
|
2633
|
+
"""(path, settings dict or None): Amp's user settings.json as tezgah may
|
|
2634
|
+
rewrite it - absent reads as {}, and None stands for a file tezgah must not
|
|
2635
|
+
touch: one that is not plain JSON, or a settings.jsonc beside it (which of
|
|
2636
|
+
the two wins is undocumented)."""
|
|
2637
|
+
path = os.path.join(AMP, "settings.json")
|
|
2638
|
+
if os.path.exists(os.path.join(AMP, "settings.jsonc")):
|
|
2639
|
+
return path, None
|
|
2640
|
+
return path, read_json(path, None) if os.path.exists(path) else {}
|
|
2641
|
+
|
|
2642
|
+
|
|
2643
|
+
def install_amp():
|
|
2644
|
+
"""Amp: one system plugin file, `$XDG_CONFIG_HOME/amp/plugins/tezgah.ts`,
|
|
2645
|
+
rendered from `hosts/amp/tezgah-plugin.ts.in` (Amp loads every .ts there,
|
|
2646
|
+
nothing registers it); skill links in `~/.config/amp/skills`; the always-on
|
|
2647
|
+
block in `~/.config/amp/AGENTS.md`; and Amp's two commit trailers set off
|
|
2648
|
+
in settings.json unless the user set them."""
|
|
2649
|
+
print("\namp:")
|
|
2650
|
+
plugin = ta.amp_plugin(HERE)
|
|
2651
|
+
if not plugin:
|
|
2652
|
+
print("%s %s: this tree has no hosts/amp/tezgah-plugin.ts.in to render"
|
|
2653
|
+
% (NO, AMP_PLUGIN))
|
|
2654
|
+
elif write_if_changed(AMP_PLUGIN, plugin):
|
|
2655
|
+
print("%s %s (plugin: gate + ledger + Stop rule)" % (OK, AMP_PLUGIN))
|
|
2656
|
+
print("%s amp: run `plugins: reload` in a running Amp to load it" % OK)
|
|
2657
|
+
spath, settings = amp_settings()
|
|
2658
|
+
if isinstance(settings, dict):
|
|
2659
|
+
for key in AMP_ATTRIBUTION:
|
|
2660
|
+
settings.setdefault(key, False)
|
|
2661
|
+
write_json(spath, settings)
|
|
2662
|
+
print("%s %s (commit trailers off)" % (OK, spath))
|
|
2663
|
+
for key in AMP_ATTRIBUTION:
|
|
2664
|
+
if settings[key] is not False:
|
|
2665
|
+
print("%s %s keeps your %s, so the agent's commits still carry "
|
|
2666
|
+
"that trailer" % (NO, spath, key))
|
|
2667
|
+
else:
|
|
2668
|
+
print("%s %s is not plain JSON (or settings.jsonc exists): set %s to false "
|
|
2669
|
+
"there" % (NO, spath, " and ".join("`%s`" % k for k in AMP_ATTRIBUTION)))
|
|
2670
|
+
for s in SKILLS:
|
|
2671
|
+
print(link(os.path.join(HERE, "skills", s), os.path.join(AMP, "skills", s)))
|
|
2672
|
+
print("%s %s (always-on contract)" % (OK, write_rules("amp")))
|
|
2673
|
+
|
|
2674
|
+
|
|
2675
|
+
|
|
2029
2676
|
def dsh_app_mcp_rows():
|
|
2030
2677
|
"""One dsh-mcp-client row per MCP server tezgah writes into the block.
|
|
2031
2678
|
|
|
@@ -2410,7 +3057,32 @@ CLAUDE_RULES = os.path.join(CLAUDE, "CLAUDE.md")
|
|
|
2410
3057
|
# and where that file is. A lambda per row, because codex_rules() answers from
|
|
2411
3058
|
# the disk (AGENTS.override.md wins when it exists).
|
|
2412
3059
|
STATIC_RULES = {"claude": lambda: CLAUDE_RULES, "codex": codex_rules,
|
|
2413
|
-
"omp": lambda: os.path.join(OMP_AGENT, "RULES.md")
|
|
3060
|
+
"omp": lambda: os.path.join(OMP_AGENT, "RULES.md"),
|
|
3061
|
+
"qwen": lambda: os.path.join(QWEN, "QWEN.md"),
|
|
3062
|
+
"droid": lambda: os.path.join(DROID, "AGENTS.md"),
|
|
3063
|
+
"commandcode": lambda: os.path.join(COMMANDCODE, "AGENTS.md"),
|
|
3064
|
+
"copilot": lambda: os.path.join(COPILOT, "copilot-instructions.md"),
|
|
3065
|
+
"gemini": lambda: os.path.join(GEMINI, "GEMINI.md"),
|
|
3066
|
+
"antigravity": lambda: os.path.join(os.path.dirname(ANTIGRAVITY), "GEMINI.md"),
|
|
3067
|
+
"goose": lambda: os.path.join(GOOSE, ".goosehints"),
|
|
3068
|
+
"amp": lambda: os.path.join(AMP, "AGENTS.md")}
|
|
3069
|
+
# Gemini CLI and Antigravity both load ~/.gemini/GEMINI.md, so they share ONE
|
|
3070
|
+
# managed block: an uninstall strips it only while no other host of this tuple
|
|
3071
|
+
# that reads the same file stays armed (`gemini_md_kept`; GEMINI_CLI_HOME moves
|
|
3072
|
+
# Gemini's copy away), and each host's uninstall asks the same question, so the
|
|
3073
|
+
# last one out takes the block.
|
|
3074
|
+
GEMINI_MD_HOSTS = ("gemini", "antigravity")
|
|
3075
|
+
# the hosts the running uninstall takes out (`uninstall` sets it)
|
|
3076
|
+
UNINSTALLING = ()
|
|
3077
|
+
|
|
3078
|
+
|
|
3079
|
+
def gemini_md_kept(host):
|
|
3080
|
+
"""True while another GEMINI_MD_HOSTS host this uninstall does not touch is
|
|
3081
|
+
armed and reads `host`'s GEMINI.md."""
|
|
3082
|
+
armed = tp.config().get("hosts") or []
|
|
3083
|
+
path = STATIC_RULES[host]()
|
|
3084
|
+
return any(h in armed and h not in UNINSTALLING and STATIC_RULES[h]() == path
|
|
3085
|
+
for h in GEMINI_MD_HOSTS)
|
|
2414
3086
|
|
|
2415
3087
|
|
|
2416
3088
|
def rules_block(host):
|
|
@@ -2525,7 +3197,11 @@ def install_omp():
|
|
|
2525
3197
|
|
|
2526
3198
|
INSTALLERS = {"claude": install_claude, "codex": install_codex,
|
|
2527
3199
|
"opencode": install_opencode, "cursor": install_cursor,
|
|
2528
|
-
"dsh": install_dsh, "omp": install_omp
|
|
3200
|
+
"dsh": install_dsh, "omp": install_omp, "qwen": install_qwen,
|
|
3201
|
+
"droid": install_droid, "commandcode": install_commandcode,
|
|
3202
|
+
"copilot": install_copilot, "gemini": install_gemini,
|
|
3203
|
+
"antigravity": install_antigravity, "goose": install_goose,
|
|
3204
|
+
"amp": install_amp}
|
|
2529
3205
|
|
|
2530
3206
|
|
|
2531
3207
|
def install_openresearch(hosts):
|
|
@@ -2945,6 +3621,20 @@ def _run_bounded(argv, timeout):
|
|
|
2945
3621
|
except OSError:
|
|
2946
3622
|
pass
|
|
2947
3623
|
proc.wait()
|
|
3624
|
+
# a member mid-fork as the kill swept the group can come out of the
|
|
3625
|
+
# fork unsignalled (macOS under load: the orphan held the caller's
|
|
3626
|
+
# pipes, plan 115), so signal the group again until it is empty. Each
|
|
3627
|
+
# round catches what forked since the last and a healthy group is gone
|
|
3628
|
+
# within a few; the 2 s cap only bounds one that will not die (a member
|
|
3629
|
+
# stuck in uninterruptible I/O, a zombie nobody reaps) so the Ctrl-C
|
|
3630
|
+
# still returns.
|
|
3631
|
+
deadline = time.monotonic() + 2
|
|
3632
|
+
while time.monotonic() < deadline:
|
|
3633
|
+
try:
|
|
3634
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
3635
|
+
except OSError: # ESRCH: nothing left in the group
|
|
3636
|
+
break
|
|
3637
|
+
time.sleep(0.01)
|
|
2948
3638
|
raise
|
|
2949
3639
|
|
|
2950
3640
|
|
|
@@ -3363,9 +4053,12 @@ def verify_uninstall(hosts, full):
|
|
|
3363
4053
|
if "opencode" in hosts:
|
|
3364
4054
|
claim("opencode plugin unlinked",
|
|
3365
4055
|
[os.path.join(OPENCODE, d, "tezgah.js") for d in ("plugins", "plugin")]
|
|
3366
|
-
+ [os.path.join(OPENCODE, "tui-plugins",
|
|
4056
|
+
+ [os.path.join(OPENCODE, "tui-plugins", n)
|
|
4057
|
+
for n in ("tezgah-tui.tsx", "tezgah-cli")])
|
|
3367
4058
|
clean("opencode opencode.json unwired",
|
|
3368
4059
|
[os.path.join(OPENCODE, "opencode.json")])
|
|
4060
|
+
clean("opencode cli.json unwired", [os.path.join(OPENCODE, "cli.json")],
|
|
4061
|
+
needle=OPENCODE_CLI_PLUGIN)
|
|
3369
4062
|
claim("opencode generated routers gone", [SKILL_ROUTER, SKILL_ROUTER_FULL])
|
|
3370
4063
|
if "cursor" in hosts:
|
|
3371
4064
|
clean("cursor hooks.json unwired", [os.path.join(CURSOR, "hooks.json")])
|
|
@@ -3388,6 +4081,67 @@ def verify_uninstall(hosts, full):
|
|
|
3388
4081
|
claim("omp skills unlinked",
|
|
3389
4082
|
[os.path.join(OMP_AGENT, "skills", s) for s in SKILLS
|
|
3390
4083
|
if os.path.lexists(os.path.join(OMP_AGENT, "skills", s))])
|
|
4084
|
+
if "qwen" in hosts:
|
|
4085
|
+
clean("qwen settings.json unwired", [os.path.join(QWEN, "settings.json")])
|
|
4086
|
+
clean("qwen global QWEN.md block gone", [os.path.join(QWEN, "QWEN.md")],
|
|
4087
|
+
needle="<!-- tezgah:start")
|
|
4088
|
+
claim("qwen skills unlinked",
|
|
4089
|
+
[os.path.join(QWEN, "skills", s) for s in SKILLS
|
|
4090
|
+
if os.path.lexists(os.path.join(QWEN, "skills", s))])
|
|
4091
|
+
if "droid" in hosts:
|
|
4092
|
+
clean("droid hooks.json unwired", [os.path.join(DROID, "hooks.json")])
|
|
4093
|
+
clean("droid settings.json statusLine gone", [os.path.join(DROID, "settings.json")],
|
|
4094
|
+
needle="tezgah-statusline")
|
|
4095
|
+
clean("droid global AGENTS.md block gone", [os.path.join(DROID, "AGENTS.md")],
|
|
4096
|
+
needle="<!-- tezgah:start")
|
|
4097
|
+
claim("droid skills unlinked",
|
|
4098
|
+
[os.path.join(DROID, "skills", s) for s in SKILLS
|
|
4099
|
+
if os.path.lexists(os.path.join(DROID, "skills", s))])
|
|
4100
|
+
if "commandcode" in hosts:
|
|
4101
|
+
clean("commandcode settings.json unwired",
|
|
4102
|
+
[os.path.join(COMMANDCODE, "settings.json")])
|
|
4103
|
+
clean("commandcode global AGENTS.md block gone",
|
|
4104
|
+
[os.path.join(COMMANDCODE, "AGENTS.md")], needle="<!-- tezgah:start")
|
|
4105
|
+
claim("commandcode skills unlinked",
|
|
4106
|
+
[os.path.join(COMMANDCODE, "skills", s) for s in SKILLS
|
|
4107
|
+
if os.path.lexists(os.path.join(COMMANDCODE, "skills", s))])
|
|
4108
|
+
if "copilot" in hosts:
|
|
4109
|
+
claim("copilot hooks/tezgah.json gone",
|
|
4110
|
+
[os.path.join(COPILOT, "hooks", "tezgah.json")])
|
|
4111
|
+
clean("copilot settings.json statusLine gone",
|
|
4112
|
+
[os.path.join(COPILOT, "settings.json")], needle="tezgah-statusline")
|
|
4113
|
+
clean("copilot copilot-instructions.md block gone",
|
|
4114
|
+
[os.path.join(COPILOT, "copilot-instructions.md")], needle="<!-- tezgah:start")
|
|
4115
|
+
claim("copilot skills unlinked",
|
|
4116
|
+
[os.path.join(COPILOT, "skills", s) for s in SKILLS
|
|
4117
|
+
if os.path.lexists(os.path.join(COPILOT, "skills", s))])
|
|
4118
|
+
if "gemini" in hosts:
|
|
4119
|
+
claim("gemini extension dir gone", [GEMINI_EXT])
|
|
4120
|
+
if not gemini_md_kept("gemini"):
|
|
4121
|
+
clean("gemini GEMINI.md block gone", [os.path.join(GEMINI, "GEMINI.md")],
|
|
4122
|
+
needle="<!-- tezgah:start")
|
|
4123
|
+
if "antigravity" in hosts:
|
|
4124
|
+
clean("antigravity config/hooks.json unwired", [AGY_HOOKS],
|
|
4125
|
+
needle="tezgah-antigravity-hook")
|
|
4126
|
+
clean("antigravity settings.json statusLine gone",
|
|
4127
|
+
[os.path.join(ANTIGRAVITY, "settings.json")], needle="tezgah-statusline")
|
|
4128
|
+
claim("antigravity skills unlinked",
|
|
4129
|
+
[os.path.join(ANTIGRAVITY, "skills", s) for s in SKILLS
|
|
4130
|
+
if os.path.lexists(os.path.join(ANTIGRAVITY, "skills", s))])
|
|
4131
|
+
if not gemini_md_kept("antigravity"):
|
|
4132
|
+
clean("antigravity GEMINI.md block gone", [STATIC_RULES["antigravity"]()],
|
|
4133
|
+
needle="<!-- tezgah:start")
|
|
4134
|
+
if "goose" in hosts:
|
|
4135
|
+
claim("goose plugin dir gone", [GOOSE_PLUGIN])
|
|
4136
|
+
clean("goose .goosehints block gone", [STATIC_RULES["goose"]()],
|
|
4137
|
+
needle="<!-- tezgah:start")
|
|
4138
|
+
if "amp" in hosts:
|
|
4139
|
+
claim("amp plugin gone", [AMP_PLUGIN] if amp_plugin_ours() else [])
|
|
4140
|
+
claim("amp skills unlinked",
|
|
4141
|
+
[os.path.join(AMP, "skills", s) for s in SKILLS
|
|
4142
|
+
if os.path.lexists(os.path.join(AMP, "skills", s))])
|
|
4143
|
+
clean("amp AGENTS.md block gone", [STATIC_RULES["amp"]()],
|
|
4144
|
+
needle="<!-- tezgah:start")
|
|
3391
4145
|
if "claude" in hosts:
|
|
3392
4146
|
clean("claude settings.json unwired",
|
|
3393
4147
|
[os.path.join(CLAUDE, "settings.json")])
|
|
@@ -3499,6 +4253,14 @@ def unwire_opencode_config():
|
|
|
3499
4253
|
tj.pop("plugin", None)
|
|
3500
4254
|
write_json(t, tj)
|
|
3501
4255
|
print("%s removed tezgah TUI plugin from %s" % (OK, t))
|
|
4256
|
+
cp = os.path.join(OPENCODE, "cli.json")
|
|
4257
|
+
cj = read_json(cp, None)
|
|
4258
|
+
if cj and isinstance(cj.get("plugins"), list):
|
|
4259
|
+
kept = [x for x in cj["plugins"] if x != "file://" + OPENCODE_CLI_PLUGIN]
|
|
4260
|
+
if kept != cj["plugins"]:
|
|
4261
|
+
cj["plugins"] = kept
|
|
4262
|
+
write_json(cp, cj)
|
|
4263
|
+
print("%s removed tezgah CLI plugin from %s" % (OK, cp))
|
|
3502
4264
|
|
|
3503
4265
|
|
|
3504
4266
|
def uninstall_claude():
|
|
@@ -3560,7 +4322,8 @@ def uninstall_opencode():
|
|
|
3560
4322
|
unlink_tezgah(os.path.join(OPENCODE, "skills", s))
|
|
3561
4323
|
for d in ("plugins", "plugin"):
|
|
3562
4324
|
unlink_tezgah(os.path.join(OPENCODE, d, "tezgah.js"))
|
|
3563
|
-
|
|
4325
|
+
for name in ("tezgah-tui.tsx", "tezgah-cli"):
|
|
4326
|
+
unlink_tezgah(os.path.join(OPENCODE, "tui-plugins", name))
|
|
3564
4327
|
unwire_opencode_config()
|
|
3565
4328
|
for path in (SKILL_ROUTER, SKILL_ROUTER_FULL):
|
|
3566
4329
|
try:
|
|
@@ -3594,6 +4357,213 @@ def uninstall_cursor():
|
|
|
3594
4357
|
print("%s removed tezgah statusLine from %s" % (OK, cc))
|
|
3595
4358
|
|
|
3596
4359
|
|
|
4360
|
+
def uninstall_qwen():
|
|
4361
|
+
print("\nqwen:")
|
|
4362
|
+
for s in SKILLS:
|
|
4363
|
+
unlink_tezgah(os.path.join(QWEN, "skills", s))
|
|
4364
|
+
path = os.path.join(QWEN, "settings.json")
|
|
4365
|
+
drop_managed_hook_entries(path)
|
|
4366
|
+
cfg = read_json(path, None)
|
|
4367
|
+
ui = cfg.get("ui") if isinstance(cfg, dict) else None
|
|
4368
|
+
if isinstance(ui, dict) and "tezgah-statusline" in json.dumps(ui.get("statusLine") or {}):
|
|
4369
|
+
ui.pop("statusLine")
|
|
4370
|
+
if not ui:
|
|
4371
|
+
cfg.pop("ui")
|
|
4372
|
+
write_json(path, cfg)
|
|
4373
|
+
print("%s removed tezgah statusLine from %s" % (OK, path))
|
|
4374
|
+
drop_if_empty(path)
|
|
4375
|
+
strip_managed_md(os.path.join(QWEN, "QWEN.md"))
|
|
4376
|
+
|
|
4377
|
+
|
|
4378
|
+
def drop_if_empty(path):
|
|
4379
|
+
"""Remove a host settings file uninstall emptied: tezgah created it to hold
|
|
4380
|
+
its hooks or status line, and a `{}` the user never had is left over."""
|
|
4381
|
+
if read_json(path, None) == {}:
|
|
4382
|
+
backup(path)
|
|
4383
|
+
os.remove(path)
|
|
4384
|
+
print("%s removed %s (empty once tezgah's keys left)" % (OK, path))
|
|
4385
|
+
|
|
4386
|
+
|
|
4387
|
+
def uninstall_droid():
|
|
4388
|
+
print("\ndroid:")
|
|
4389
|
+
for s in SKILLS:
|
|
4390
|
+
unlink_tezgah(os.path.join(DROID, "skills", s))
|
|
4391
|
+
path = os.path.join(DROID, "hooks.json")
|
|
4392
|
+
data = read_json(path, None)
|
|
4393
|
+
if isinstance(data, dict):
|
|
4394
|
+
kept = {}
|
|
4395
|
+
for ev, arr in data.items():
|
|
4396
|
+
if isinstance(arr, list):
|
|
4397
|
+
arr = [e for e in arr if "tezgah" not in json.dumps(e)]
|
|
4398
|
+
if not arr:
|
|
4399
|
+
continue
|
|
4400
|
+
kept[ev] = arr
|
|
4401
|
+
if kept == data:
|
|
4402
|
+
print("%s no tezgah hook entries in %s" % (OK, path))
|
|
4403
|
+
elif kept:
|
|
4404
|
+
write_json(path, kept)
|
|
4405
|
+
print("%s removed tezgah hook entries from %s" % (OK, path))
|
|
4406
|
+
else:
|
|
4407
|
+
# an empty hooks.json would still hide the `hooks` key of settings.json
|
|
4408
|
+
backup(path)
|
|
4409
|
+
os.remove(path)
|
|
4410
|
+
print("%s removed %s (it held only tezgah's hooks)" % (OK, path))
|
|
4411
|
+
spath = os.path.join(DROID, "settings.json")
|
|
4412
|
+
cfg = read_json(spath, None)
|
|
4413
|
+
if isinstance(cfg, dict) and "tezgah-statusline" in json.dumps(cfg.get("statusLine") or {}):
|
|
4414
|
+
cfg.pop("statusLine")
|
|
4415
|
+
write_json(spath, cfg)
|
|
4416
|
+
print("%s removed tezgah statusLine from %s" % (OK, spath))
|
|
4417
|
+
elif cfg is None and os.path.exists(spath):
|
|
4418
|
+
# JSON with comments: take out the one line install_droid put in, keep
|
|
4419
|
+
# the comments; `includeCoAuthoredByDroid` stays false (the
|
|
4420
|
+
# no-attribution rule is the user's, not tezgah's to undo)
|
|
4421
|
+
with open(spath, encoding="utf-8") as fh:
|
|
4422
|
+
text = fh.read()
|
|
4423
|
+
old = (jsonc_loads(text) or {}).get("statusLine")
|
|
4424
|
+
new = jsonc_drop(text, "statusLine", old) if "tezgah-statusline" in json.dumps(old or "") else None
|
|
4425
|
+
if new is not None:
|
|
4426
|
+
write_text(spath, new)
|
|
4427
|
+
print("%s removed tezgah statusLine from %s" % (OK, spath))
|
|
4428
|
+
drop_if_empty(spath)
|
|
4429
|
+
strip_managed_md(os.path.join(DROID, "AGENTS.md"))
|
|
4430
|
+
|
|
4431
|
+
|
|
4432
|
+
def uninstall_commandcode():
|
|
4433
|
+
print("\ncommandcode:")
|
|
4434
|
+
for s in SKILLS:
|
|
4435
|
+
unlink_tezgah(os.path.join(COMMANDCODE, "skills", s))
|
|
4436
|
+
path = os.path.join(COMMANDCODE, "settings.json")
|
|
4437
|
+
drop_managed_hook_entries(path)
|
|
4438
|
+
cfg = read_json(path, None)
|
|
4439
|
+
attr = cfg.get("attribution") if isinstance(cfg, dict) else None
|
|
4440
|
+
# the value install wrote, beside the user's other attribution keys or
|
|
4441
|
+
# alone; as with Claude's, a user's own identical value cannot be told
|
|
4442
|
+
# apart from it, and dropping it only restores Command Code's default
|
|
4443
|
+
if isinstance(attr, dict) and attr.get("commit") == "":
|
|
4444
|
+
attr.pop("commit")
|
|
4445
|
+
if not attr:
|
|
4446
|
+
cfg.pop("attribution")
|
|
4447
|
+
write_json(path, cfg)
|
|
4448
|
+
print("%s removed tezgah's attribution.commit from %s" % (OK, path))
|
|
4449
|
+
drop_if_empty(path)
|
|
4450
|
+
strip_managed_md(os.path.join(COMMANDCODE, "AGENTS.md"))
|
|
4451
|
+
|
|
4452
|
+
|
|
4453
|
+
def uninstall_copilot():
|
|
4454
|
+
print("\ncopilot:")
|
|
4455
|
+
for s in SKILLS:
|
|
4456
|
+
unlink_tezgah(os.path.join(COPILOT, "skills", s))
|
|
4457
|
+
path = os.path.join(COPILOT, "hooks", "tezgah.json")
|
|
4458
|
+
if os.path.exists(path):
|
|
4459
|
+
backup(path)
|
|
4460
|
+
os.remove(path)
|
|
4461
|
+
print("%s removed %s" % (OK, path))
|
|
4462
|
+
spath = os.path.join(COPILOT, "settings.json")
|
|
4463
|
+
cfg = read_json(spath, None)
|
|
4464
|
+
if isinstance(cfg, dict):
|
|
4465
|
+
if "tezgah-statusline" in json.dumps(cfg.get("statusLine") or {}):
|
|
4466
|
+
cfg.pop("statusLine")
|
|
4467
|
+
# the value install wrote; a user's own false cannot be told apart from it
|
|
4468
|
+
if cfg.get("includeCoAuthoredBy") is False:
|
|
4469
|
+
cfg.pop("includeCoAuthoredBy")
|
|
4470
|
+
if write_json(spath, cfg):
|
|
4471
|
+
print("%s removed tezgah's statusLine and includeCoAuthoredBy from %s"
|
|
4472
|
+
% (OK, spath))
|
|
4473
|
+
drop_if_empty(spath)
|
|
4474
|
+
strip_managed_md(os.path.join(COPILOT, "copilot-instructions.md"))
|
|
4475
|
+
|
|
4476
|
+
|
|
4477
|
+
def uninstall_gemini():
|
|
4478
|
+
"""Delete tezgah's extension dir (its skills are links, unlinked, never
|
|
4479
|
+
followed) and the GEMINI.md block unless Antigravity still reads it."""
|
|
4480
|
+
print("\ngemini:")
|
|
4481
|
+
if os.path.lexists(GEMINI_EXT):
|
|
4482
|
+
for name in ("gemini-extension.json", os.path.join("hooks", "hooks.json")):
|
|
4483
|
+
backup(os.path.join(GEMINI_EXT, name))
|
|
4484
|
+
shutil.rmtree(GEMINI_EXT)
|
|
4485
|
+
print("%s removed %s" % (OK, GEMINI_EXT))
|
|
4486
|
+
if gemini_md_kept("gemini"):
|
|
4487
|
+
print("%s %s keeps the tezgah block: another armed host reads it"
|
|
4488
|
+
% (OK, os.path.join(GEMINI, "GEMINI.md")))
|
|
4489
|
+
else:
|
|
4490
|
+
strip_managed_md(os.path.join(GEMINI, "GEMINI.md"))
|
|
4491
|
+
|
|
4492
|
+
|
|
4493
|
+
def uninstall_antigravity():
|
|
4494
|
+
"""Take the `tezgah` name out of ~/.gemini/config/hooks.json (the user's
|
|
4495
|
+
other names stay), the statusLine, the skill links, and the GEMINI.md block
|
|
4496
|
+
unless Gemini CLI still reads it."""
|
|
4497
|
+
print("\nantigravity:")
|
|
4498
|
+
for s in SKILLS:
|
|
4499
|
+
unlink_tezgah(os.path.join(ANTIGRAVITY, "skills", s))
|
|
4500
|
+
data = read_json(AGY_HOOKS, None)
|
|
4501
|
+
if isinstance(data, dict) and "tezgah" in data:
|
|
4502
|
+
data.pop("tezgah")
|
|
4503
|
+
write_json(AGY_HOOKS, data)
|
|
4504
|
+
print("%s removed tezgah's hooks from %s" % (OK, AGY_HOOKS))
|
|
4505
|
+
drop_if_empty(AGY_HOOKS)
|
|
4506
|
+
spath = os.path.join(ANTIGRAVITY, "settings.json")
|
|
4507
|
+
cfg = read_json(spath, None)
|
|
4508
|
+
if isinstance(cfg, dict) and "tezgah-statusline" in json.dumps(cfg.get("statusLine") or {}):
|
|
4509
|
+
cfg.pop("statusLine")
|
|
4510
|
+
write_json(spath, cfg)
|
|
4511
|
+
print("%s removed tezgah's statusLine from %s" % (OK, spath))
|
|
4512
|
+
drop_if_empty(spath)
|
|
4513
|
+
rules = STATIC_RULES["antigravity"]()
|
|
4514
|
+
if gemini_md_kept("antigravity"):
|
|
4515
|
+
print("%s %s keeps the tezgah block: another armed host reads it" % (OK, rules))
|
|
4516
|
+
else:
|
|
4517
|
+
strip_managed_md(rules)
|
|
4518
|
+
|
|
4519
|
+
|
|
4520
|
+
def uninstall_goose():
|
|
4521
|
+
"""Delete tezgah's plugin dir (its skills are links, unlinked, never
|
|
4522
|
+
followed) and the .goosehints block; the rest of ~/.agents is the user's."""
|
|
4523
|
+
print("\ngoose:")
|
|
4524
|
+
if os.path.lexists(GOOSE_PLUGIN):
|
|
4525
|
+
for name in ("plugin.json", os.path.join("hooks", "hooks.json")):
|
|
4526
|
+
backup(os.path.join(GOOSE_PLUGIN, name))
|
|
4527
|
+
shutil.rmtree(GOOSE_PLUGIN)
|
|
4528
|
+
print("%s removed %s" % (OK, GOOSE_PLUGIN))
|
|
4529
|
+
strip_managed_md(STATIC_RULES["goose"]())
|
|
4530
|
+
|
|
4531
|
+
|
|
4532
|
+
def amp_plugin_ours():
|
|
4533
|
+
"""True when the file at AMP_PLUGIN is tezgah's: a fresh render, or one an
|
|
4534
|
+
older tree rendered (it starts with AMP_HEADER). A user's own plugin of
|
|
4535
|
+
that name is never removed."""
|
|
4536
|
+
try:
|
|
4537
|
+
with open(AMP_PLUGIN, encoding="utf-8") as fh:
|
|
4538
|
+
text = fh.read()
|
|
4539
|
+
except OSError:
|
|
4540
|
+
return False
|
|
4541
|
+
return text == ta.amp_plugin(HERE) or text.startswith(ta.AMP_HEADER)
|
|
4542
|
+
|
|
4543
|
+
|
|
4544
|
+
def uninstall_amp():
|
|
4545
|
+
"""Delete tezgah's plugin file (only when it is tezgah's), the skill links,
|
|
4546
|
+
the AGENTS.md block, and each commit-trailer key while it is still false."""
|
|
4547
|
+
print("\namp:")
|
|
4548
|
+
if amp_plugin_ours():
|
|
4549
|
+
backup(AMP_PLUGIN)
|
|
4550
|
+
os.remove(AMP_PLUGIN)
|
|
4551
|
+
print("%s removed %s" % (OK, AMP_PLUGIN))
|
|
4552
|
+
elif os.path.lexists(AMP_PLUGIN):
|
|
4553
|
+
print("%s %s is not tezgah's render: left in place" % (NO, AMP_PLUGIN))
|
|
4554
|
+
for s in SKILLS:
|
|
4555
|
+
unlink_tezgah(os.path.join(AMP, "skills", s))
|
|
4556
|
+
spath, settings = amp_settings()
|
|
4557
|
+
if isinstance(settings, dict) and any(settings.get(k) is False for k in AMP_ATTRIBUTION):
|
|
4558
|
+
for key in AMP_ATTRIBUTION:
|
|
4559
|
+
if settings.get(key) is False:
|
|
4560
|
+
settings.pop(key)
|
|
4561
|
+
write_json(spath, settings)
|
|
4562
|
+
print("%s removed tezgah's commit-trailer keys from %s" % (OK, spath))
|
|
4563
|
+
drop_if_empty(spath)
|
|
4564
|
+
strip_managed_md(STATIC_RULES["amp"]())
|
|
4565
|
+
|
|
4566
|
+
|
|
3597
4567
|
def uninstall_dsh():
|
|
3598
4568
|
print("\ndsh:")
|
|
3599
4569
|
for s in SKILLS:
|
|
@@ -3646,7 +4616,11 @@ def uninstall_omp():
|
|
|
3646
4616
|
|
|
3647
4617
|
UNINSTALLERS = {"claude": uninstall_claude, "codex": uninstall_codex,
|
|
3648
4618
|
"opencode": uninstall_opencode, "cursor": uninstall_cursor,
|
|
3649
|
-
"dsh": uninstall_dsh, "omp": uninstall_omp
|
|
4619
|
+
"dsh": uninstall_dsh, "omp": uninstall_omp, "qwen": uninstall_qwen,
|
|
4620
|
+
"droid": uninstall_droid, "commandcode": uninstall_commandcode,
|
|
4621
|
+
"copilot": uninstall_copilot, "gemini": uninstall_gemini,
|
|
4622
|
+
"antigravity": uninstall_antigravity, "goose": uninstall_goose,
|
|
4623
|
+
"amp": uninstall_amp}
|
|
3650
4624
|
|
|
3651
4625
|
|
|
3652
4626
|
def uninstall(hosts, dry_run=False):
|
|
@@ -3668,6 +4642,7 @@ def uninstall(hosts, dry_run=False):
|
|
|
3668
4642
|
repository's `.tezgah/` research state. A partial run (some armed hosts
|
|
3669
4643
|
stay) also keeps config.json and the install tree, because the hosts still
|
|
3670
4644
|
armed read both at runtime."""
|
|
4645
|
+
global UNINSTALLING
|
|
3671
4646
|
armed = tp.config().get("hosts") or []
|
|
3672
4647
|
full = not armed or set(hosts) >= set(armed)
|
|
3673
4648
|
if dry_run:
|
|
@@ -3699,6 +4674,7 @@ def uninstall(hosts, dry_run=False):
|
|
|
3699
4674
|
print("%s gate stays armed for the hosts this run does not touch" % OK)
|
|
3700
4675
|
print("uninstalling for: %s (%s run)\n"
|
|
3701
4676
|
% (", ".join(hosts) or "none detected", "full" if full else "partial"))
|
|
4677
|
+
UNINSTALLING = tuple(hosts)
|
|
3702
4678
|
uninstall_common()
|
|
3703
4679
|
for h in hosts:
|
|
3704
4680
|
UNINSTALLERS[h]()
|
|
@@ -3714,6 +4690,13 @@ def uninstall(hosts, dry_run=False):
|
|
|
3714
4690
|
remove_install_tree()
|
|
3715
4691
|
elif armed:
|
|
3716
4692
|
rest = sorted(set(armed) - set(hosts))
|
|
4693
|
+
# install_common only ever merges into the record, so this is the one
|
|
4694
|
+
# place a host leaves it; kept, it made every later uninstall partial
|
|
4695
|
+
# and `tezgah update` re-armed it from the record
|
|
4696
|
+
cfg = read_config(tp.CONFIG, {})
|
|
4697
|
+
if cfg is not None:
|
|
4698
|
+
cfg["hosts"] = [h for h in armed if h in rest]
|
|
4699
|
+
write_json(tp.CONFIG, cfg)
|
|
3717
4700
|
print("\n%s still armed: %s keeps its config and the install tree"
|
|
3718
4701
|
% (NO, ", ".join(rest)))
|
|
3719
4702
|
remaining = verify_uninstall(hosts, full)
|
|
@@ -4034,12 +5017,13 @@ def context_budget_report():
|
|
|
4034
5017
|
|
|
4035
5018
|
LIVE_TIMEOUT = 20
|
|
4036
5019
|
LIVE_DENY_COMMAND = "git commit --no-verify -m x"
|
|
4037
|
-
# The JS half: loads the omp extension
|
|
4038
|
-
#
|
|
4039
|
-
# it), registers its handlers against
|
|
4040
|
-
#
|
|
4041
|
-
#
|
|
4042
|
-
# root, session id, command
|
|
5020
|
+
# The JS half: loads the omp extension or the Amp plugin
|
|
5021
|
+
# (`--experimental-strip-types`, the .ts as their bun would) or the opencode
|
|
5022
|
+
# plugin (an ES module, as opencode imports it), registers its handlers against
|
|
5023
|
+
# inert host objects and fires one handler: the tool-result one, or with a
|
|
5024
|
+
# command the tool-call one, printing what it answered (omp, Amp) or what it
|
|
5025
|
+
# threw (opencode) as JSON. argv: host, module path, root, session id, command
|
|
5026
|
+
# ("" for the tool result).
|
|
4043
5027
|
LIVE_DRIVER = r"""
|
|
4044
5028
|
const [host, mod, root, sid, cmd] = process.argv.slice(1);
|
|
4045
5029
|
const noop = new Proxy(function () {}, { get: () => noop, apply: () => noop });
|
|
@@ -4060,6 +5044,25 @@ const noop = new Proxy(function () {}, { get: () => noop, apply: () => noop });
|
|
|
4060
5044
|
}
|
|
4061
5045
|
await handlers.tool_result({ toolName: "bash", input: { command: "true" },
|
|
4062
5046
|
isError: false, content: "ok" }, ctx);
|
|
5047
|
+
} else if (host === "amp") {
|
|
5048
|
+
// Amp's registry, its two helpers the plugin reads, and a thread context;
|
|
5049
|
+
// no status item (`experimental` absent), so no status spawn either
|
|
5050
|
+
const handlers = {};
|
|
5051
|
+
m.default(new Proxy({ on: (n, fn) => { handlers[n] = fn; }, experimental: undefined,
|
|
5052
|
+
system: { workspaceRoot: root },
|
|
5053
|
+
helpers: { filePathFromURI: (u) => String(u),
|
|
5054
|
+
shellCommandFromToolCall: (e) => ({ command: e.input.cmd }) } },
|
|
5055
|
+
{ get: (t, k) => (k in t ? t[k] : noop) }));
|
|
5056
|
+
const want = cmd ? "tool.call" : "tool.result";
|
|
5057
|
+
if (!handlers[want]) throw new Error("the plugin registered no " + want + " handler");
|
|
5058
|
+
const ctx = { thread: { id: sid, parentThreadID: async () => null }, ui: noop };
|
|
5059
|
+
const event = { tool: "Bash", input: { cmd: cmd || "true" }, toolUseID: "live",
|
|
5060
|
+
thread: { id: sid } };
|
|
5061
|
+
if (cmd) {
|
|
5062
|
+
process.stdout.write(JSON.stringify((await handlers[want](event, ctx)) || {}));
|
|
5063
|
+
return;
|
|
5064
|
+
}
|
|
5065
|
+
await handlers[want](Object.assign(event, { status: "done", output: "ok" }), ctx);
|
|
4063
5066
|
} else {
|
|
4064
5067
|
const hooks = await (m.Tezgah || m.default)({ directory: root, worktree: root,
|
|
4065
5068
|
client: noop, $: noop });
|
|
@@ -4222,6 +5225,123 @@ def live_plan(host, deny=False):
|
|
|
4222
5225
|
if registered is None:
|
|
4223
5226
|
return plan + ("registration unread: the omp CLI did not answer",)
|
|
4224
5227
|
return plan
|
|
5228
|
+
if host == "qwen":
|
|
5229
|
+
# the hook command Qwen runs for its shell tool, fed Qwen's own payload;
|
|
5230
|
+
# no qwen binary and no model call (tests/e2e_qwen.py drives the binary)
|
|
5231
|
+
path = os.path.join(QWEN, "settings.json")
|
|
5232
|
+
cmd = _hook_command((read_json(path, {}).get("hooks") or {}).get(event),
|
|
5233
|
+
"run_shell_command")
|
|
5234
|
+
if not cmd:
|
|
5235
|
+
return ("miss", "no tezgah %s command for run_shell_command in %s"
|
|
5236
|
+
% (event, path))
|
|
5237
|
+
payload = {"hook_event_name": event, "tool_name": "run_shell_command",
|
|
5238
|
+
"tool_input": {"command": LIVE_DENY_COMMAND if deny else "true"}}
|
|
5239
|
+
if not deny:
|
|
5240
|
+
payload["tool_response"] = {"llmContent": "", "returnDisplay": ""}
|
|
5241
|
+
return ("shell", cmd, payload, {})
|
|
5242
|
+
if host == "droid":
|
|
5243
|
+
# the hook command Droid runs for Execute, fed Droid's own payload; no
|
|
5244
|
+
# droid binary and no model call (tests/e2e_droid.py drives the binary)
|
|
5245
|
+
path = os.path.join(DROID, "hooks.json")
|
|
5246
|
+
cmd = _hook_command(read_json(path, {}).get(event), "Execute")
|
|
5247
|
+
if not cmd:
|
|
5248
|
+
return ("miss", "no tezgah %s command for Execute in %s" % (event, path))
|
|
5249
|
+
payload = {"hook_event_name": event, "tool_name": "Execute",
|
|
5250
|
+
"permission_mode": "auto-low",
|
|
5251
|
+
"tool_input": {"command": LIVE_DENY_COMMAND if deny else "true"}}
|
|
5252
|
+
if not deny:
|
|
5253
|
+
payload["tool_response"] = "\n\n[Process exited with code 0]"
|
|
5254
|
+
return ("shell", cmd, payload, {})
|
|
5255
|
+
if host == "commandcode":
|
|
5256
|
+
# the hook command Command Code runs for its shell tool (the matcher sees
|
|
5257
|
+
# the display name SHELL, case-insensitively), fed its own payload; no
|
|
5258
|
+
# binary and no model call
|
|
5259
|
+
path = os.path.join(COMMANDCODE, "settings.json")
|
|
5260
|
+
cmd = _hook_command((read_json(path, {}).get("hooks") or {}).get(event), "shell")
|
|
5261
|
+
if not cmd:
|
|
5262
|
+
return ("miss", "no tezgah %s command for shell in %s" % (event, path))
|
|
5263
|
+
payload = {"hook_event_name": event, "tool_name": "shell_command",
|
|
5264
|
+
"tool_display_name": "SHELL", "permission_mode": "default",
|
|
5265
|
+
"tool_input": {"command": LIVE_DENY_COMMAND if deny else "true"}}
|
|
5266
|
+
if not deny:
|
|
5267
|
+
payload["tool_response"] = ""
|
|
5268
|
+
return ("shell", cmd, payload, {})
|
|
5269
|
+
if host == "copilot":
|
|
5270
|
+
# the hook command Copilot runs for its shell tool (a PascalCase
|
|
5271
|
+
# PreToolUse sees the Claude name Bash), fed its own payload; no binary
|
|
5272
|
+
# and no model call
|
|
5273
|
+
path = os.path.join(COPILOT, "hooks", "tezgah.json")
|
|
5274
|
+
cmd = _hook_command((read_json(path, {}).get("hooks") or {}).get(event), "Bash",
|
|
5275
|
+
own=True)
|
|
5276
|
+
if not cmd:
|
|
5277
|
+
return ("miss", "no tezgah %s command for Bash in %s" % (event, path))
|
|
5278
|
+
payload = {"hook_event_name": event, "tool_name": "Bash",
|
|
5279
|
+
"tool_input": {"command": LIVE_DENY_COMMAND if deny else "true"}}
|
|
5280
|
+
if not deny:
|
|
5281
|
+
payload["tool_result"] = {"result_type": "success",
|
|
5282
|
+
"text_result_for_llm": "<exited with exit code 0>"}
|
|
5283
|
+
return ("shell", cmd, payload, {})
|
|
5284
|
+
if host == "gemini":
|
|
5285
|
+
# the hook command Gemini runs for its shell tool (BeforeTool/AfterTool),
|
|
5286
|
+
# fed Gemini's own payload; no binary and no model call
|
|
5287
|
+
path = os.path.join(GEMINI_EXT, "hooks", "hooks.json")
|
|
5288
|
+
name = "BeforeTool" if deny else "AfterTool"
|
|
5289
|
+
cmd = _hook_command((read_json(path, {}).get("hooks") or {}).get(name),
|
|
5290
|
+
"run_shell_command", own=True)
|
|
5291
|
+
if not cmd:
|
|
5292
|
+
return ("miss", "no tezgah %s command for run_shell_command in %s"
|
|
5293
|
+
% (name, path))
|
|
5294
|
+
payload = {"hook_event_name": name, "tool_name": "run_shell_command",
|
|
5295
|
+
"tool_input": {"command": LIVE_DENY_COMMAND if deny else "true"}}
|
|
5296
|
+
if not deny:
|
|
5297
|
+
payload["tool_response"] = {"llmContent": "Output: (empty)",
|
|
5298
|
+
"returnDisplay": ""}
|
|
5299
|
+
return ("shell", cmd, payload, {})
|
|
5300
|
+
if host == "antigravity":
|
|
5301
|
+
# the hook command Antigravity runs for run_command, under the `tezgah`
|
|
5302
|
+
# name of ~/.gemini/config/hooks.json; it carries the event as argv[1],
|
|
5303
|
+
# since the camelCase payload names none. No binary and no model call
|
|
5304
|
+
cmd = _hook_command((read_json(AGY_HOOKS, {}).get("tezgah") or {}).get(event),
|
|
5305
|
+
"run_command", own=True)
|
|
5306
|
+
if not cmd:
|
|
5307
|
+
return ("miss", "no tezgah %s command for run_command in %s" % (event, AGY_HOOKS))
|
|
5308
|
+
payload = {"toolCall": {"name": "run_command", "args": {
|
|
5309
|
+
"CommandLine": LIVE_DENY_COMMAND if deny else "true", "WaitMsBeforeAsync": 5000}},
|
|
5310
|
+
"stepIdx": 0}
|
|
5311
|
+
if not deny:
|
|
5312
|
+
payload["error"] = ""
|
|
5313
|
+
return ("shell", cmd, payload, {})
|
|
5314
|
+
if host == "goose":
|
|
5315
|
+
# the hook command Goose runs for its `shell` tool, from tezgah's plugin
|
|
5316
|
+
# hooks.json, fed Goose's own payload (the event in `event`, the
|
|
5317
|
+
# directory as `working_dir`); no binary and no model call
|
|
5318
|
+
path = os.path.join(GOOSE_PLUGIN, "hooks", "hooks.json")
|
|
5319
|
+
cmd = _hook_command((read_json(path, {}).get("hooks") or {}).get(event), "shell",
|
|
5320
|
+
own=True)
|
|
5321
|
+
if not cmd:
|
|
5322
|
+
return ("miss", "no tezgah %s command for shell in %s" % (event, path))
|
|
5323
|
+
return ("shell", cmd, {"event": event, "tool_name": "shell",
|
|
5324
|
+
"matcher_context": "shell", "tool_call_id": "tezgah-live",
|
|
5325
|
+
"tool_input": {"command": LIVE_DENY_COMMAND if deny
|
|
5326
|
+
else "true"}}, {})
|
|
5327
|
+
if host == "amp":
|
|
5328
|
+
# the installed plugin itself, loaded by node through LIVE_DRIVER and fed
|
|
5329
|
+
# Amp's own event shape (`tool`, `input.cmd`, `thread.id`); the hook it
|
|
5330
|
+
# spawns is the one Amp would spawn
|
|
5331
|
+
try:
|
|
5332
|
+
with open(AMP_PLUGIN, encoding="utf-8") as fh:
|
|
5333
|
+
text = fh.read()
|
|
5334
|
+
except OSError:
|
|
5335
|
+
return ("miss", "no plugin at %s" % AMP_PLUGIN)
|
|
5336
|
+
if text != ta.amp_plugin(HERE):
|
|
5337
|
+
return ("miss", "%s differs from a fresh render of this tree's plugin "
|
|
5338
|
+
"- re-run `tezgah-setup --install --hosts amp`" % AMP_PLUGIN)
|
|
5339
|
+
node = _node_for_ts()
|
|
5340
|
+
if not node:
|
|
5341
|
+
return ("unverified", "no node 22.6+ to load %s" % AMP_PLUGIN)
|
|
5342
|
+
# always unverified: Amp's own Bun loader is not what ran it
|
|
5343
|
+
return ("node", [node, "--experimental-strip-types", "--no-warnings"], AMP_PLUGIN,
|
|
5344
|
+
"Amp's own plugin loader is not driven here")
|
|
4225
5345
|
return ("unverified", "no live probe for this host")
|
|
4226
5346
|
|
|
4227
5347
|
|
|
@@ -4239,6 +5359,21 @@ def _denied(host, stdout):
|
|
|
4239
5359
|
return got.get("block") is True
|
|
4240
5360
|
if host == "opencode":
|
|
4241
5361
|
return bool(got.get("threw"))
|
|
5362
|
+
if host == "copilot":
|
|
5363
|
+
# top level, not wrapped (hooks-reference "preToolUse decision control")
|
|
5364
|
+
return got.get("permissionDecision") == "deny"
|
|
5365
|
+
if host == "gemini":
|
|
5366
|
+
# top level (hooks reference "BeforeTool"; `block` is its alias)
|
|
5367
|
+
return got.get("decision") in ("deny", "block")
|
|
5368
|
+
if host == "antigravity":
|
|
5369
|
+
# top level; `decision` is required, so an allow answers too (docs/hooks)
|
|
5370
|
+
return got.get("decision") == "deny"
|
|
5371
|
+
if host == "goose":
|
|
5372
|
+
# top level, stdout starting with `{` (hooks "Blocking a Tool Call")
|
|
5373
|
+
return got.get("decision") == "block"
|
|
5374
|
+
if host == "amp":
|
|
5375
|
+
# ToolCallResult (plugin-api): the call is refused, the turn goes on
|
|
5376
|
+
return got.get("action") == "reject-and-continue"
|
|
4242
5377
|
return (got.get("hookSpecificOutput") or {}).get("permissionDecision") == "deny"
|
|
4243
5378
|
|
|
4244
5379
|
|
|
@@ -4271,7 +5406,13 @@ def live_probe(host, deny=False):
|
|
|
4271
5406
|
if plan[0] == "shell":
|
|
4272
5407
|
_kind, cmd, payload, extra = plan
|
|
4273
5408
|
env.update(extra)
|
|
4274
|
-
|
|
5409
|
+
# Antigravity's envelope is camelCase, with no session_id or cwd;
|
|
5410
|
+
# Goose names the directory `working_dir`
|
|
5411
|
+
payload = (dict(payload, conversationId=sid, workspacePaths=[root])
|
|
5412
|
+
if host == "antigravity" else
|
|
5413
|
+
dict(payload, session_id=sid, working_dir=root)
|
|
5414
|
+
if host == "goose" else
|
|
5415
|
+
dict(payload, session_id=sid, conversation_id=sid, cwd=root))
|
|
4275
5416
|
proc = subprocess.run(cmd, shell=True, input=json.dumps(payload),
|
|
4276
5417
|
capture_output=True, text=True, env=env,
|
|
4277
5418
|
cwd=root, timeout=LIVE_TIMEOUT)
|
|
@@ -4344,7 +5485,9 @@ ARMING_ROWS = ("hooks.json wired", "plugin wired (link or copy)",
|
|
|
4344
5485
|
"carries the contract", "contract in instructions",
|
|
4345
5486
|
"claude-code hooks bridge in cordis.patch.yml",
|
|
4346
5487
|
"session, prompt, tool and stop hooks wired",
|
|
4347
|
-
"hook registered in omp extensions", "plugin copy current"
|
|
5488
|
+
"hook registered in omp extensions", "plugin copy current",
|
|
5489
|
+
"settings.json hooks wired", "hooks/tezgah.json wired",
|
|
5490
|
+
"plugin tezgah.ts current")
|
|
4348
5491
|
|
|
4349
5492
|
|
|
4350
5493
|
def arming_miss(label, good):
|
|
@@ -4376,6 +5519,8 @@ def report(hosts):
|
|
|
4376
5519
|
("codegen provider key (OpenRouter, DeepSeek or Inception)",
|
|
4377
5520
|
tp.have_provider_key()),
|
|
4378
5521
|
("orx on PATH (OpenResearch research routing)", bool(tp.orx_bin())),
|
|
5522
|
+
("ocr on PATH (open-code-review spec for the reviewer, opt-in)",
|
|
5523
|
+
bool(tp.ocr_bin())),
|
|
4379
5524
|
("npx on PATH (app-analysis MCP servers)", bool(shutil.which("npx"))),
|
|
4380
5525
|
("app artifacts dir %s" % tezgah_apps.ARTIFACTS,
|
|
4381
5526
|
os.path.isdir(tezgah_apps.ARTIFACTS)),
|
|
@@ -4608,14 +5753,17 @@ def claude_mcp_rows(copies):
|
|
|
4608
5753
|
return rows
|
|
4609
5754
|
|
|
4610
5755
|
|
|
4611
|
-
def _hooks_has(path, marker, events=None):
|
|
5756
|
+
def _hooks_has(path, marker, events=None, bare=False, key="hooks"):
|
|
4612
5757
|
"""True when `marker` is wired under every event in `events`, or under any
|
|
4613
|
-
event when `events` is None.
|
|
5758
|
+
event when `events` is None. `bare`: the file is keyed by event at the top
|
|
5759
|
+
(Droid's hooks.json); `key`: the top-level name the events sit under
|
|
5760
|
+
(`hooks`, or Antigravity's `tezgah`).
|
|
4614
5761
|
|
|
4615
5762
|
A row that one event can satisfy answers the wrong question: a manifest
|
|
4616
5763
|
carrying tezgah's command under SessionStart and not under Stop reads as
|
|
4617
5764
|
wired, and Stop is the integrity refusal."""
|
|
4618
|
-
|
|
5765
|
+
data = read_json(path, {}) or {}
|
|
5766
|
+
hooks = data if bare else data.get(key) or {}
|
|
4619
5767
|
if events is None:
|
|
4620
5768
|
return any(marker in json.dumps(e) for arr in hooks.values() for e in arr)
|
|
4621
5769
|
return all(any(marker in json.dumps(e) for e in (hooks.get(ev) or []))
|
|
@@ -4694,6 +5842,7 @@ def host_checks_codex():
|
|
|
4694
5842
|
def host_checks_opencode():
|
|
4695
5843
|
cfg = read_json(os.path.join(OPENCODE, "opencode.json"), {})
|
|
4696
5844
|
tui = read_json(os.path.join(OPENCODE, "tui.json"), {})
|
|
5845
|
+
cli = read_json(os.path.join(OPENCODE, "cli.json"), {})
|
|
4697
5846
|
instr = cfg.get("instructions") or []
|
|
4698
5847
|
specs = [p if isinstance(p, str) else p[0] for p in (tui.get("plugin") or [])]
|
|
4699
5848
|
return [
|
|
@@ -4711,14 +5860,20 @@ def host_checks_opencode():
|
|
|
4711
5860
|
] + mcp_rows(set(cfg.get("mcp") or {}), "opencode.json") + [
|
|
4712
5861
|
mcp_row(TEZGAH_MCP_NAME, (cfg.get("mcp") or {}).get(TEZGAH_MCP_NAME),
|
|
4713
5862
|
"opencode.json", tezgah_mcp_command()),
|
|
5863
|
+
] + ([
|
|
5864
|
+
("status line plugin in cli.json + linked",
|
|
5865
|
+
"file://" + OPENCODE_CLI_PLUGIN in (cli.get("plugins") or [])
|
|
5866
|
+
and os.path.exists(OPENCODE_CLI_PLUGIN)),
|
|
5867
|
+
] if opencode_v2() else [
|
|
4714
5868
|
("context pruning armed (compaction.prune)",
|
|
4715
5869
|
(cfg.get("compaction") or {}).get("prune") is True),
|
|
4716
|
-
("watcher ignores noisy trees",
|
|
4717
|
-
bool(set(OPENCODE_IGNORE)
|
|
4718
|
-
& set(((cfg.get("watcher") or {}).get("ignore") or [])))),
|
|
4719
5870
|
("status line plugin in tui.json + linked",
|
|
4720
5871
|
any("tezgah-tui" in s for s in specs)
|
|
4721
5872
|
and os.path.exists(os.path.join(OPENCODE, "tui-plugins", "tezgah-tui.tsx"))),
|
|
5873
|
+
]) + [
|
|
5874
|
+
("watcher ignores noisy trees",
|
|
5875
|
+
bool(set(OPENCODE_IGNORE)
|
|
5876
|
+
& set(((cfg.get("watcher") or {}).get("ignore") or [])))),
|
|
4722
5877
|
# opencode's own registry declares INCEPTION_API_KEY for the inception
|
|
4723
5878
|
# provider, so nothing needs adding to opencode.json - but the route is
|
|
4724
5879
|
# only usable once a key resolves, and that is what this reports.
|
|
@@ -4747,6 +5902,157 @@ def host_checks_cursor():
|
|
|
4747
5902
|
]
|
|
4748
5903
|
|
|
4749
5904
|
|
|
5905
|
+
def host_checks_qwen():
|
|
5906
|
+
path = os.path.join(QWEN, "settings.json")
|
|
5907
|
+
# a relocated home (QWEN_HOME) is named, as codex's is
|
|
5908
|
+
where = " in %s" % QWEN if QWEN != os.path.join(tp.HOME, ".qwen") else ""
|
|
5909
|
+
ui = read_json(path, {}).get("ui")
|
|
5910
|
+
return [
|
|
5911
|
+
("settings.json hooks wired%s" % where,
|
|
5912
|
+
_hooks_has(path, "tezgah-qwen-hook", [ev for ev, _m, _t in QWEN_HOOK_EVENTS])),
|
|
5913
|
+
("skills linked%s" % where, skills_linked(os.path.join(QWEN, "skills"))),
|
|
5914
|
+
("QWEN.md carries the contract (matches a fresh render)", rules_current("qwen")),
|
|
5915
|
+
("statusLine in settings.json ui",
|
|
5916
|
+
"tezgah-statusline" in json.dumps((ui if isinstance(ui, dict) else {})
|
|
5917
|
+
.get("statusLine") or {})),
|
|
5918
|
+
("qwen CLI on PATH", bool(tp.which_user("qwen"))),
|
|
5919
|
+
]
|
|
5920
|
+
|
|
5921
|
+
|
|
5922
|
+
def host_checks_droid():
|
|
5923
|
+
path = os.path.join(DROID, "hooks.json")
|
|
5924
|
+
why = droid_hooks_blocked()
|
|
5925
|
+
settings = read_jsonc(os.path.join(DROID, "settings.json")) or {}
|
|
5926
|
+
status = settings.get("statusLine")
|
|
5927
|
+
return [
|
|
5928
|
+
("hooks.json wired" + (" (not written: %s)" % why if why else ""),
|
|
5929
|
+
_hooks_has(path, "tezgah-droid-hook", [ev for ev, _m, _t in DROID_HOOK_EVENTS],
|
|
5930
|
+
bare=True)),
|
|
5931
|
+
("skills linked", skills_linked(os.path.join(DROID, "skills"))),
|
|
5932
|
+
("AGENTS.md carries the contract (matches a fresh render)", rules_current("droid")),
|
|
5933
|
+
("statusLine in settings.json", "tezgah-statusline" in json.dumps(status or {})),
|
|
5934
|
+
("includeCoAuthoredByDroid off in settings.json",
|
|
5935
|
+
settings.get("includeCoAuthoredByDroid") is False),
|
|
5936
|
+
("droid CLI on PATH", bool(tp.which_user("droid"))),
|
|
5937
|
+
]
|
|
5938
|
+
|
|
5939
|
+
|
|
5940
|
+
def host_checks_commandcode():
|
|
5941
|
+
path = os.path.join(COMMANDCODE, "settings.json")
|
|
5942
|
+
attr = read_json(path, {}).get("attribution")
|
|
5943
|
+
return [
|
|
5944
|
+
("settings.json hooks wired",
|
|
5945
|
+
_hooks_has(path, "tezgah-commandcode-hook",
|
|
5946
|
+
[ev for ev, _m, _t in COMMANDCODE_HOOK_EVENTS])),
|
|
5947
|
+
("skills linked", skills_linked(os.path.join(COMMANDCODE, "skills"))),
|
|
5948
|
+
("AGENTS.md carries the contract (matches a fresh render)",
|
|
5949
|
+
rules_current("commandcode")),
|
|
5950
|
+
("attribution.commit off in settings.json",
|
|
5951
|
+
isinstance(attr, dict) and attr.get("commit") == ""),
|
|
5952
|
+
("command-code CLI on PATH", bool(tp.which_user("command-code"))),
|
|
5953
|
+
]
|
|
5954
|
+
|
|
5955
|
+
|
|
5956
|
+
def host_checks_copilot():
|
|
5957
|
+
# a relocated home (COPILOT_HOME) is named, as codex's is
|
|
5958
|
+
where = " in %s" % COPILOT if COPILOT != os.path.join(tp.HOME, ".copilot") else ""
|
|
5959
|
+
settings = read_jsonc(os.path.join(COPILOT, "settings.json")) or {}
|
|
5960
|
+
return [
|
|
5961
|
+
("hooks/tezgah.json wired%s" % where,
|
|
5962
|
+
_hooks_has(os.path.join(COPILOT, "hooks", "tezgah.json"), "tezgah-copilot-hook",
|
|
5963
|
+
[ev for ev, _m, _t in COPILOT_HOOK_EVENTS])),
|
|
5964
|
+
("skills linked%s" % where, skills_linked(os.path.join(COPILOT, "skills"))),
|
|
5965
|
+
("copilot-instructions.md carries the contract (matches a fresh render)",
|
|
5966
|
+
rules_current("copilot")),
|
|
5967
|
+
("statusLine in settings.json",
|
|
5968
|
+
"tezgah-statusline" in json.dumps(settings.get("statusLine") or {})),
|
|
5969
|
+
("includeCoAuthoredBy off in settings.json",
|
|
5970
|
+
settings.get("includeCoAuthoredBy") is False),
|
|
5971
|
+
("copilot CLI on PATH", bool(tp.which_user("copilot"))),
|
|
5972
|
+
]
|
|
5973
|
+
|
|
5974
|
+
|
|
5975
|
+
def host_checks_gemini():
|
|
5976
|
+
# a relocated home (GEMINI_CLI_HOME) is named, as codex's is
|
|
5977
|
+
where = " in %s" % GEMINI if GEMINI != os.path.join(tp.HOME, ".gemini") else ""
|
|
5978
|
+
hooks_cfg = (read_jsonc(os.path.join(GEMINI, "settings.json")) or {}).get("hooksConfig")
|
|
5979
|
+
return [
|
|
5980
|
+
# the hooks load only beside a manifest naming the extension
|
|
5981
|
+
("extension hooks/hooks.json wired%s" % where,
|
|
5982
|
+
_hooks_has(os.path.join(GEMINI_EXT, "hooks", "hooks.json"), "tezgah-gemini-hook",
|
|
5983
|
+
[ev for ev, _m, _t in GEMINI_HOOK_EVENTS])
|
|
5984
|
+
and read_json(os.path.join(GEMINI_EXT, "gemini-extension.json"), {})
|
|
5985
|
+
.get("name") == "tezgah"),
|
|
5986
|
+
("skills linked%s" % where, skills_linked(os.path.join(GEMINI_EXT, "skills"))),
|
|
5987
|
+
("GEMINI.md carries the contract (matches a fresh render)", rules_current("gemini")),
|
|
5988
|
+
# `hooksConfig.enabled: false` runs no hook at all (configuration)
|
|
5989
|
+
("hooks left on in settings.json (hooksConfig.enabled)",
|
|
5990
|
+
not (isinstance(hooks_cfg, dict) and hooks_cfg.get("enabled") is False)),
|
|
5991
|
+
("gemini CLI on PATH", bool(tp.which_user("gemini"))),
|
|
5992
|
+
]
|
|
5993
|
+
|
|
5994
|
+
|
|
5995
|
+
def host_checks_antigravity():
|
|
5996
|
+
settings = read_json(os.path.join(ANTIGRAVITY, "settings.json"), {})
|
|
5997
|
+
mine = read_json(AGY_HOOKS, {}).get("tezgah")
|
|
5998
|
+
return [
|
|
5999
|
+
("config/hooks.json wired (the `tezgah` name)",
|
|
6000
|
+
_hooks_has(AGY_HOOKS, "tezgah-antigravity-hook",
|
|
6001
|
+
[ev for ev, _m, _t in ANTIGRAVITY_HOOK_EVENTS], key="tezgah")),
|
|
6002
|
+
# `enabled: false` on a hook name loads none of its events (docs/hooks)
|
|
6003
|
+
("tezgah hook set left enabled in config/hooks.json",
|
|
6004
|
+
not (isinstance(mine, dict) and mine.get("enabled") is False)),
|
|
6005
|
+
("skills linked", skills_linked(os.path.join(ANTIGRAVITY, "skills"))),
|
|
6006
|
+
("GEMINI.md carries the contract (matches a fresh render)",
|
|
6007
|
+
rules_current("antigravity")),
|
|
6008
|
+
("statusLine in antigravity-cli/settings.json",
|
|
6009
|
+
"tezgah-statusline" in json.dumps(settings.get("statusLine") or {})),
|
|
6010
|
+
("agy CLI on PATH", bool(tp.which_user("agy"))),
|
|
6011
|
+
]
|
|
6012
|
+
|
|
6013
|
+
|
|
6014
|
+
def host_checks_goose():
|
|
6015
|
+
disabled = read_json(os.path.join(GOOSE, "settings.json"), {}).get("disabledPlugins")
|
|
6016
|
+
return [
|
|
6017
|
+
# the hooks load only from a plugin dir under ~/.agents/plugins
|
|
6018
|
+
("plugin hooks/hooks.json wired (~/.agents/plugins/tezgah)",
|
|
6019
|
+
_hooks_has(os.path.join(GOOSE_PLUGIN, "hooks", "hooks.json"), "tezgah-goose-hook",
|
|
6020
|
+
[ev for ev, _m, _t in GOOSE_HOOK_EVENTS])
|
|
6021
|
+
and read_json(os.path.join(GOOSE_PLUGIN, "plugin.json"), {}).get("name") == "tezgah"),
|
|
6022
|
+
# a plugin named in `disabledPlugins` is skipped at discovery (plugins guide)
|
|
6023
|
+
("tezgah plugin not in disabledPlugins (settings.json)",
|
|
6024
|
+
not (isinstance(disabled, list) and "tezgah" in disabled)),
|
|
6025
|
+
("skills linked", skills_linked(os.path.join(GOOSE_PLUGIN, "skills"))),
|
|
6026
|
+
(".goosehints carries the contract (matches a fresh render)", rules_current("goose")),
|
|
6027
|
+
("goose CLI on PATH", bool(tp.which_user("goose"))),
|
|
6028
|
+
]
|
|
6029
|
+
|
|
6030
|
+
|
|
6031
|
+
def host_checks_amp():
|
|
6032
|
+
try:
|
|
6033
|
+
with open(AMP_PLUGIN, encoding="utf-8") as fh:
|
|
6034
|
+
plugin = fh.read()
|
|
6035
|
+
except OSError:
|
|
6036
|
+
plugin = ""
|
|
6037
|
+
# either user settings file; the comment-tolerant reader for both
|
|
6038
|
+
settings = (read_jsonc(os.path.join(AMP, "settings.json")) or {}
|
|
6039
|
+
if not os.path.exists(os.path.join(AMP, "settings.jsonc"))
|
|
6040
|
+
else read_jsonc(os.path.join(AMP, "settings.jsonc")) or {})
|
|
6041
|
+
return [
|
|
6042
|
+
# byte for byte, as omp's bridge row: a hand edit or an older tree's
|
|
6043
|
+
# render is not the wiring this tree arms
|
|
6044
|
+
("plugin tezgah.ts current (matches a fresh render): %s" % AMP_PLUGIN,
|
|
6045
|
+
bool(plugin) and plugin == ta.amp_plugin(HERE)),
|
|
6046
|
+
("skills linked", skills_linked(os.path.join(AMP, "skills"))),
|
|
6047
|
+
("AGENTS.md carries the contract (matches a fresh render)", rules_current("amp")),
|
|
6048
|
+
("%s off in settings.json" % AMP_ATTRIBUTION[0],
|
|
6049
|
+
settings.get(AMP_ATTRIBUTION[0]) is False),
|
|
6050
|
+
("%s off in settings.json" % AMP_ATTRIBUTION[1],
|
|
6051
|
+
settings.get(AMP_ATTRIBUTION[1]) is False),
|
|
6052
|
+
("amp CLI on PATH", bool(tp.which_user("amp"))),
|
|
6053
|
+
]
|
|
6054
|
+
|
|
6055
|
+
|
|
4750
6056
|
def host_checks_dsh():
|
|
4751
6057
|
try:
|
|
4752
6058
|
with open(DSH_PATCH, encoding="utf-8") as fh:
|
|
@@ -4865,7 +6171,11 @@ def host_checks_omp():
|
|
|
4865
6171
|
|
|
4866
6172
|
HOST_CHECKS = {"claude": host_checks_claude, "codex": host_checks_codex,
|
|
4867
6173
|
"opencode": host_checks_opencode, "cursor": host_checks_cursor,
|
|
4868
|
-
"dsh": host_checks_dsh, "omp": host_checks_omp
|
|
6174
|
+
"dsh": host_checks_dsh, "omp": host_checks_omp, "qwen": host_checks_qwen,
|
|
6175
|
+
"droid": host_checks_droid, "commandcode": host_checks_commandcode,
|
|
6176
|
+
"copilot": host_checks_copilot, "gemini": host_checks_gemini,
|
|
6177
|
+
"antigravity": host_checks_antigravity, "goose": host_checks_goose,
|
|
6178
|
+
"amp": host_checks_amp}
|
|
4869
6179
|
|
|
4870
6180
|
|
|
4871
6181
|
def versions_agree():
|
|
@@ -5923,7 +7233,7 @@ def hosts_row():
|
|
|
5923
7233
|
host's first HOST_CHECKS row), as one report row. Reported, never changed:
|
|
5924
7234
|
`--upgrade`, `--enable`/`--disable` and a full uninstall act on the recorded
|
|
5925
7235
|
list, so a host wired but not recorded is one they skip, and which list is
|
|
5926
|
-
right is the user's call (`--install --hosts ...`
|
|
7236
|
+
right is the user's call (`--install --hosts ...` adds to it)."""
|
|
5927
7237
|
recorded = sorted(tp.config().get("hosts") or [])
|
|
5928
7238
|
wired = sorted(h for h in ALL_HOSTS if HOST_CHECKS[h]()[0][1])
|
|
5929
7239
|
return ("config.json hosts (%s) match the hosts wired (%s)"
|