@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
|
@@ -31,6 +31,7 @@ import re
|
|
|
31
31
|
import shlex
|
|
32
32
|
import statistics
|
|
33
33
|
import time
|
|
34
|
+
import urllib.parse
|
|
34
35
|
|
|
35
36
|
try:
|
|
36
37
|
import fcntl
|
|
@@ -49,12 +50,17 @@ import tezgah_store as ts
|
|
|
49
50
|
VERIFY = re.compile(
|
|
50
51
|
r"(?:^|[|;&(])\s*"
|
|
51
52
|
r"(?:(?:sudo|env|time|nohup|command|exec)\s+(?:\w+=\S+\s+)*(?:-\S+\s+)*)?"
|
|
52
|
-
r"(?:" r"(?:python3?|uv run)\s+(
|
|
53
|
-
r"(?:[\w./-]*/)?tezgah-design\s+(?:check|derive)|"
|
|
53
|
+
r"(?:" r"(?:python3?|uv run)\s+(?:(?:--project|--with|--python|--extra|--group|-p|-w)\s+\S+\s+|-\S+\s+)*(?:python3?\s+-m\s+)?(?:pytest|unittest|mypy|ruff|flake8|compileall|"
|
|
54
|
+
r"(?:[\w./-]*/)?tezgah-design\s+(?:check|derive|slop)|"
|
|
54
55
|
r"(?:[\w./-]*/)?tests[/\\]impacted\.py)|"
|
|
55
56
|
r"pytest|py\.test|"
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
# A runner may carry a flag (and its value) before its task: a monorepo's
|
|
58
|
+
# `pnpm --filter mobile test` was read as a plain command, so the check it
|
|
59
|
+
# ran recorded as `run` and the Stop rule refused the claim it supported
|
|
60
|
+
# (plan 135: 11 of the post-2026-10-07 window's blocked claims, 2 before).
|
|
61
|
+
r"(?:npm|yarn|pnpm|bun)\s+(?:(?:--filter|--prefix|--workspace|--cwd|--dir|-C|-w)\s+\S+\s+|-\S+\s+)*"
|
|
62
|
+
r"(?:test\b|t\b|run\s+(?:-\S+\s+)*\S*(?:test|lint|typecheck|check|build|ci)|"
|
|
63
|
+
r"workspace\s+\S+\s+(?:test\b|t\b|run\s+\S*(?:test|lint|typecheck|check|build|ci)))|"
|
|
58
64
|
r"ruff|flake8|mypy|pyright|tsc|eslint|prettier|"
|
|
59
65
|
r"vitest|jest|ava|mocha|"
|
|
60
66
|
r"go\s+(?:test|vet)|"
|
|
@@ -66,12 +72,12 @@ VERIFY = re.compile(
|
|
|
66
72
|
r"swift\s+test|golangci-lint|shellcheck|"
|
|
67
73
|
r"playwright|cypress|storybook|chromatic|percy|lighthouse|pa11y|backstop|"
|
|
68
74
|
r"axe-core|reg-suit|"
|
|
69
|
-
r"(?:[\w./-]*/)?tezgah-design\s+(?:check|derive)|"
|
|
75
|
+
r"(?:[\w./-]*/)?tezgah-design\s+(?:check|derive|slop)|"
|
|
70
76
|
r"(?:[\w./-]*/)?tests[/\\]impacted\.py|"
|
|
71
77
|
# a JS tool behind its runner is still the check, and the name it returns is
|
|
72
78
|
# the tool's, not the runner's: `npx axe-core` matched as `axe-core` before
|
|
73
79
|
# the anchor was tightened, so the runner is outside the name group here
|
|
74
|
-
r"(?:npx|npm(?:\s+run)?|pnpm(?:\s+(?:exec|dlx|run))?|yarn|bunx?|uvx)"
|
|
80
|
+
r"(?:npx|npm(?:\s+run)?|pnpm(?:\s+(?:(?:--filter|--prefix|--workspace|-C|-w)\s+\S+\s+|-\S+\s+)*(?:exec|dlx|run))?|yarn|bunx?|uvx)"
|
|
75
81
|
r"\s+(?:-\S+\s+)*(?P<js>playwright|cypress|storybook|chromatic|percy|"
|
|
76
82
|
r"lighthouse|pa11y|axe-core|backstop|reg-suit|vitest|jest|ava|mocha|eslint|"
|
|
77
83
|
r"prettier|tsc|pyright)"
|
|
@@ -168,8 +174,14 @@ UI_TOOL_CMD = re.compile(
|
|
|
168
174
|
# is read at a command position for `UI_CHECK`'s reason - a grep whose pattern is
|
|
169
175
|
# `tezgah-design check` is text ABOUT the checker. The interpreter prefix is the
|
|
170
176
|
# one the module's own refusal text and the skill both print.
|
|
177
|
+
DESIGN_SLOP = re.compile(r"(?:^|[|;&(])\s*(?:python3?\s+|uv\s+run\s+)?(?:\S*/)?"
|
|
178
|
+
r"tezgah-design\s+slop\b", re.I | re.M)
|
|
179
|
+
# `slop --repo <path>`: the tree a verdict scanned
|
|
180
|
+
SLOP_REPO = re.compile(r"tezgah-design\s+slop\b[^|;&]*?--repo\s+([^\s|;&]+)",
|
|
181
|
+
re.I)
|
|
171
182
|
DESIGN_CHECK = re.compile(
|
|
172
|
-
r"(?:^|[|;&(])\s*(?:python3?\s+|uv\s+run\s+)?(?:\S*/)?tezgah-design\s+
|
|
183
|
+
r"(?:^|[|;&(])\s*(?:python3?\s+|uv\s+run\s+)?(?:\S*/)?tezgah-design\s+"
|
|
184
|
+
r"(?:check|slop)\b",
|
|
173
185
|
re.I | re.M)
|
|
174
186
|
# A component, as opposed to the screen it sits on: a file under a
|
|
175
187
|
# component/views/widgets directory, or a name that carries the convention on its
|
|
@@ -224,6 +236,9 @@ ROUGH_WORDS = re.compile(r"[;&|()]+|[^\s;&|()<>]+")
|
|
|
224
236
|
# a redirection operator, or `&` in front of one (`&>`); shlex splits
|
|
225
237
|
# `2>&1` into `2`, `>`, `&`, `1`, and none of those is an argument
|
|
226
238
|
REDIRECTION = re.compile(r"^(?=[&<>]*[<>])[&<>]+$")
|
|
239
|
+
# an fd number glued to its redirection (`2>`, `1<&`): bash reads `1<` as fd 1
|
|
240
|
+
# and `1 <` as the program `1`, and shlex cannot tell them apart afterwards
|
|
241
|
+
FD_REDIRECT = re.compile(r"(?<![\w])(\d+)([<>])")
|
|
227
242
|
# what a shell line puts between the start of a command and its program; the
|
|
228
243
|
# same set in the JS mirror, so the two read a line identically
|
|
229
244
|
GIT_WRAPPER = frozenset(("sudo", "env", "nohup", "time", "timeout", "command",
|
|
@@ -372,13 +387,22 @@ SHAPE_BLOCKS = frozenset(("placating opener", "forbidden closer", "list cap",
|
|
|
372
387
|
# agent could decide is no false completion. Counted in `blocked_claims` under
|
|
373
388
|
# their own class, kept out of `false_completion`.
|
|
374
389
|
LIST_BLOCKS = frozenset(("residual work", "ready steps", "stopped slice",
|
|
375
|
-
"needless ask"))
|
|
390
|
+
"needless ask", "handed step"))
|
|
391
|
+
# The host permission modes in which the session can neither write nor run: a
|
|
392
|
+
# plan-mode turn reads and proposes, so the step it names is the user's to
|
|
393
|
+
# approve, not one it could have taken (`read_only_turn`). Claude, Command
|
|
394
|
+
# Code and Qwen spell it `plan`, Droid `spec`; omp's extension sends `plan`
|
|
395
|
+
# from `sessionModes.getState()`. A host that sends no mode reads as `default`,
|
|
396
|
+
# which is what every rule did before the field.
|
|
397
|
+
READ_ONLY_MODES = frozenset(("plan", "spec"))
|
|
376
398
|
|
|
377
399
|
WRITE_TOOLS = ("edit", "write", "multiedit", "notebookedit", "apply_patch",
|
|
378
400
|
"str_replace_editor", "create_file", "str_replace", "edit_file",
|
|
379
401
|
"write_file", "search_replace")
|
|
402
|
+
# `monitor` is Claude's and Qwen's background watch: its `command` is a shell
|
|
403
|
+
# command (review of plan 138), so every shell rule reads it.
|
|
380
404
|
BASH_TOOLS = ("bash", "shell", "command", "exec_command", "run_command",
|
|
381
|
-
"powershell", "pwsh")
|
|
405
|
+
"powershell", "pwsh", "monitor")
|
|
382
406
|
# The read/search tools: known calls that do no step of work and that no rule
|
|
383
407
|
# reads a row for, spelled as the hosts send them (Claude/Cursor's capitalized
|
|
384
408
|
# set, omp's lowercase one, codex's `grep`). They are not `unknown` - the ledger
|
|
@@ -502,7 +526,7 @@ LEDGER_FIELDS = frozenset(("id", "exit", "out_bytes", "fail_class", "workspace",
|
|
|
502
526
|
"check", "key", "block", "repo", "empty_run", "cause",
|
|
503
527
|
"host", "switches", "harness", "authorized", "inflight",
|
|
504
528
|
"reask", "open", "ready", "blocked", "heads", "residual",
|
|
505
|
-
"questions", "defaults", "taken", "partial"))
|
|
529
|
+
"questions", "defaults", "taken", "partial", "mode"))
|
|
506
530
|
|
|
507
531
|
# The row contract's own version, stamped by the writer beside `kind` and `ts` so
|
|
508
532
|
# it is not a caller field. It exists because a row is read back to decide a
|
|
@@ -1125,16 +1149,49 @@ def prior_calls(session_id, digest, tail=200, agent=None):
|
|
|
1125
1149
|
turn[-1].get("fail_class"))
|
|
1126
1150
|
|
|
1127
1151
|
|
|
1152
|
+
# Any `scheme://`: an internal channel (`agent://`, `local://`), a remote URL.
|
|
1153
|
+
# `file://` is read by `file_url_path`, the rest name no file on this disk.
|
|
1154
|
+
URI_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://")
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def file_url_path(text):
|
|
1158
|
+
"""The local path a `file://` URL names, `text` itself when it carries no
|
|
1159
|
+
URI scheme, or None for any other URI and for a file URL that names no local
|
|
1160
|
+
file (a host other than `localhost`, no path, a malformed host). Read as
|
|
1161
|
+
`<cwd>/file:/x`, a URL into another checkout landed in the cwd's, one file
|
|
1162
|
+
spelled both ways was two, and `agent://<id>` was a product file (plan 116).
|
|
1163
|
+
ponytail: a shell redirect `> file:///x` writes `./file:/x`; it is read as
|
|
1164
|
+
`/x` here, a spelling no tool produces."""
|
|
1165
|
+
if not URI_SCHEME.match(text):
|
|
1166
|
+
return text
|
|
1167
|
+
if text[:7].lower() != "file://":
|
|
1168
|
+
return None
|
|
1169
|
+
try:
|
|
1170
|
+
parts = urllib.parse.urlsplit(text)
|
|
1171
|
+
except ValueError: # an unclosed `[` host
|
|
1172
|
+
return None
|
|
1173
|
+
if parts.netloc.lower() not in ("", "localhost") or not parts.path:
|
|
1174
|
+
return None
|
|
1175
|
+
return urllib.parse.unquote(parts.path)
|
|
1176
|
+
|
|
1177
|
+
|
|
1128
1178
|
def _abs_target(path, cwd):
|
|
1129
|
-
"""`path` as an absolute real path, resolved against `cwd` when relative
|
|
1179
|
+
"""`path` as an absolute real path, resolved against `cwd` when relative, a
|
|
1180
|
+
local `file://` URL as the file it names; a URI that names no local file
|
|
1181
|
+
(`s3://b/k`, `agent://<id>`, a remote file URL) as its own spelling.
|
|
1130
1182
|
|
|
1131
1183
|
The form both halves of the cross-session write guard compare: the writer
|
|
1132
1184
|
stores it in an `edit` row's `target`, the gate resolves its own call's path
|
|
1133
1185
|
the same way. A file that does not exist yet still resolves (realpath leaves
|
|
1134
|
-
the missing tail as it is), so a new file two sessions both create matches
|
|
1135
|
-
|
|
1136
|
-
|
|
1186
|
+
the missing tail as it is), so a new file two sessions both create matches,
|
|
1187
|
+
and two sessions writing one `s3://` key match on its spelling, whatever
|
|
1188
|
+
their cwds. The no-plan rule skips such a target (`file_url_path` is None)."""
|
|
1189
|
+
raw = str(path or "").strip()
|
|
1190
|
+
if not raw:
|
|
1137
1191
|
return None
|
|
1192
|
+
path = file_url_path(raw)
|
|
1193
|
+
if path is None:
|
|
1194
|
+
return raw
|
|
1138
1195
|
if not os.path.isabs(path):
|
|
1139
1196
|
path = os.path.join(cwd or os.getcwd(), path)
|
|
1140
1197
|
return os.path.realpath(path)
|
|
@@ -1459,6 +1516,23 @@ def _week(ts):
|
|
|
1459
1516
|
return (int(ts or 0) - MONDAY) // WEEK * WEEK + MONDAY
|
|
1460
1517
|
|
|
1461
1518
|
|
|
1519
|
+
def day_start(text):
|
|
1520
|
+
"""The epoch second a `YYYY-MM-DD` date starts at, in this machine's time
|
|
1521
|
+
zone, or None when the text is not that date. Whole calendar days are what
|
|
1522
|
+
a reader types and what a person reads on a calendar, so the window edges
|
|
1523
|
+
are local midnights; one format, never a guess at a date that was mistyped."""
|
|
1524
|
+
try:
|
|
1525
|
+
return int(time.mktime(time.strptime(str(text), "%Y-%m-%d")))
|
|
1526
|
+
except (TypeError, ValueError, OverflowError):
|
|
1527
|
+
return None
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def _day(ts):
|
|
1531
|
+
"""The calendar day a row's `ts` falls on, as `YYYY-MM-DD` (local, the same
|
|
1532
|
+
edges `day_start` reads)."""
|
|
1533
|
+
return time.strftime("%Y-%m-%d", time.localtime(int(ts)))
|
|
1534
|
+
|
|
1535
|
+
|
|
1462
1536
|
_CATALOG = {}
|
|
1463
1537
|
|
|
1464
1538
|
|
|
@@ -1727,6 +1801,18 @@ def _reissued(rows):
|
|
|
1727
1801
|
return out
|
|
1728
1802
|
|
|
1729
1803
|
|
|
1804
|
+
def _claim_class(detail):
|
|
1805
|
+
"""How a `claim` row's detail classifies: `(class, verdict)` where verdict is
|
|
1806
|
+
`ok` (nothing was refused), `shape` (the class in `SHAPE_BLOCKS` refuses the
|
|
1807
|
+
reply's form, not its claim) or `blocked` (the class refuses the claim).
|
|
1808
|
+
One reader for `_counts` and `rates`, so a corpus total and a window cannot
|
|
1809
|
+
disagree about what a `list cap` row is."""
|
|
1810
|
+
if not str(detail).startswith("blocked"):
|
|
1811
|
+
return None, "ok"
|
|
1812
|
+
cls = str(detail)[len("blocked: "):]
|
|
1813
|
+
return cls, ("shape" if cls in SHAPE_BLOCKS else "blocked")
|
|
1814
|
+
|
|
1815
|
+
|
|
1730
1816
|
def _counts(rows, weeks=False, tools=False):
|
|
1731
1817
|
"""`counters`' arithmetic over rows already read: the one implementation
|
|
1732
1818
|
both readers fold with, so a total and the sessions it sums cannot drift
|
|
@@ -1807,19 +1893,18 @@ def _counts(rows, weeks=False, tools=False):
|
|
|
1807
1893
|
elif kind == "claim":
|
|
1808
1894
|
out["claims"] += 1
|
|
1809
1895
|
refused = False
|
|
1810
|
-
|
|
1896
|
+
cls, verdict = _claim_class(detail)
|
|
1897
|
+
if verdict == "shape":
|
|
1811
1898
|
# a reply blocked for its shape made no false claim: the rate
|
|
1812
1899
|
# the module calls its effect must not count a list cap
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
cls
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
out["false_completion"] += 1
|
|
1822
|
-
refused = True
|
|
1900
|
+
out["shape_blocked"] += 1
|
|
1901
|
+
elif verdict == "blocked":
|
|
1902
|
+
# split by the Stop class: which check refused, as a number
|
|
1903
|
+
out["blocked_claims"][cls] = \
|
|
1904
|
+
out["blocked_claims"].get(cls, 0) + 1
|
|
1905
|
+
if cls not in LIST_BLOCKS:
|
|
1906
|
+
out["false_completion"] += 1
|
|
1907
|
+
refused = True
|
|
1823
1908
|
if weeks:
|
|
1824
1909
|
# the same bucket this row's events went into, so a claim cannot
|
|
1825
1910
|
# sit in a week's total and outside its ratio
|
|
@@ -1884,27 +1969,204 @@ def _counts(rows, weeks=False, tools=False):
|
|
|
1884
1969
|
return out
|
|
1885
1970
|
|
|
1886
1971
|
|
|
1887
|
-
|
|
1972
|
+
# The windowed rates reader (plan 134). One bucket is filled once for the window
|
|
1973
|
+
# and once for each of its calendar days from the same rows, so a day and the
|
|
1974
|
+
# window it sits in cannot disagree, and two adjacent windows partition the span
|
|
1975
|
+
# because the filter is `[start, end)` on the row's own `ts`.
|
|
1976
|
+
def _rate_bucket():
|
|
1977
|
+
"""An empty rates bucket: the counts the observation rows read, and the two
|
|
1978
|
+
histograms they split them by."""
|
|
1979
|
+
return {"events": 0, "turns": 0, "claims": 0, "replies": 0,
|
|
1980
|
+
"blocked_claims": {}, "shape_blocked": 0, "false_completion": 0,
|
|
1981
|
+
"shape_flagged": 0, "shapes": {}, "denies": {}, "row_versions": {}}
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
# A row's contract version as the rates name it, the label `failure_shapes`
|
|
1985
|
+
# already prints: `v1`, or `unversioned` for a row the writer stamped nothing on.
|
|
1986
|
+
def _contract(value):
|
|
1987
|
+
return "unversioned" if value is None else "v%s" % value
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
def _rate_row(bucket, entry):
|
|
1991
|
+
"""One ledger row into a rates bucket: the classification `_counts` makes,
|
|
1992
|
+
and only the rows these rates read."""
|
|
1993
|
+
kind = str(entry.get("kind") or "")
|
|
1994
|
+
detail = str(entry.get("detail") or "")
|
|
1995
|
+
bucket["events"] += 1
|
|
1996
|
+
bucket["row_versions"][_contract(entry.get("v"))] = \
|
|
1997
|
+
bucket["row_versions"].get(_contract(entry.get("v")), 0) + 1
|
|
1998
|
+
if kind == TURN_KIND:
|
|
1999
|
+
bucket["turns"] += 1
|
|
2000
|
+
elif kind == "deny":
|
|
2001
|
+
rule = _deny_rule(detail)
|
|
2002
|
+
bucket["denies"][rule] = bucket["denies"].get(rule, 0) + 1
|
|
2003
|
+
elif kind == "claim":
|
|
2004
|
+
bucket["claims"] += 1
|
|
2005
|
+
cls, verdict = _claim_class(detail)
|
|
2006
|
+
if verdict == "shape":
|
|
2007
|
+
bucket["shape_blocked"] += 1
|
|
2008
|
+
elif verdict == "blocked":
|
|
2009
|
+
bucket["blocked_claims"][cls] = \
|
|
2010
|
+
bucket["blocked_claims"].get(cls, 0) + 1
|
|
2011
|
+
if cls not in LIST_BLOCKS:
|
|
2012
|
+
bucket["false_completion"] += 1
|
|
2013
|
+
elif kind == "shape":
|
|
2014
|
+
# one row per judged reply, its detail the flag list (`ok` when none):
|
|
2015
|
+
# `shape_flagged` is the reply count the rate needs, `shapes` the class
|
|
2016
|
+
# split - a count of flags, so a reply carrying two moves both
|
|
2017
|
+
bucket["replies"] += 1
|
|
2018
|
+
if detail and detail != "ok":
|
|
2019
|
+
bucket["shape_flagged"] += 1
|
|
2020
|
+
for flag in (f.strip() for f in detail.split(",")):
|
|
2021
|
+
if flag:
|
|
2022
|
+
bucket["shapes"][flag] = bucket["shapes"].get(flag, 0) + 1
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def _per_scale(scale, numerator, denominator):
|
|
2026
|
+
"""`scale * n / d`, or None with no denominator: a window with no turn or no
|
|
2027
|
+
session has no rate, and a 0.0 there would read as a measured zero."""
|
|
2028
|
+
return round(scale * numerator / denominator, 4) if denominator else None
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def _window_rates(bucket, sessions):
|
|
2032
|
+
"""The rates plan 066's rows read, off a filled bucket."""
|
|
2033
|
+
blocked = sum(bucket["blocked_claims"].values())
|
|
2034
|
+
return {
|
|
2035
|
+
# plan 134's pair: what a block and a deny cost per unit of use
|
|
2036
|
+
"blocks_per_100_turns": _per_scale(100, blocked, bucket["turns"]),
|
|
2037
|
+
"denies_per_100_sessions": _per_scale(
|
|
2038
|
+
100, sum(bucket["denies"].values()), sessions),
|
|
2039
|
+
# 056's bar compares these two across adjacent windows, in pp: the share
|
|
2040
|
+
# of claims a Stop class refused, and the share of judged replies that
|
|
2041
|
+
# carried a report-only shape flag
|
|
2042
|
+
"claim_block_rate": _per_scale(1, blocked, bucket["claims"]),
|
|
2043
|
+
"stop_shape_rate": _per_scale(1, bucket["shape_flagged"],
|
|
2044
|
+
bucket["replies"]),
|
|
2045
|
+
# 051's bar is per class against live sessions: 1 per 500 reads 0.2 here
|
|
2046
|
+
"blocked_claims_per_100_sessions": {
|
|
2047
|
+
cls: _per_scale(100, n, sessions)
|
|
2048
|
+
for cls, n in bucket["blocked_claims"].items()},
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
|
|
2052
|
+
def rates(since, until):
|
|
2053
|
+
"""Every real-session ledger's rows stamped in `[since, until)`, folded per
|
|
2054
|
+
window and per calendar day: turns, `claim` rows and the Stop classes that
|
|
2055
|
+
blocked them, the shape flags every judged reply carried, denies by rule,
|
|
2056
|
+
and the rates plan 066's observation rows read (plan 134).
|
|
2057
|
+
|
|
2058
|
+
The window is this reader's; `counters_all` still folds a lifetime, because
|
|
2059
|
+
a total that contradicted the sessions it sums would be the wrong answer to
|
|
2060
|
+
its own question. `until` is exclusive, so two adjacent windows are a
|
|
2061
|
+
partition of the span and not an approximation of one - no row is counted
|
|
2062
|
+
twice or dropped at the seam. A row with no `ts` (a `ledger_damage` row
|
|
2063
|
+
stands in for bytes that could not be parsed) falls outside every window: it
|
|
2064
|
+
carries no time to place it in. `sessions` counts the real ledgers with at
|
|
2065
|
+
least one row in the window, the denominator 051's "live sessions" names;
|
|
2066
|
+
`days` is the window's own length and `by_day` holds only the days that saw
|
|
2067
|
+
a row. Both dates are `YYYY-MM-DD` local calendar days; a malformed or
|
|
2068
|
+
reversed pair is ValueError, never a silently empty window."""
|
|
2069
|
+
start, end = day_start(since), day_start(until)
|
|
2070
|
+
if start is None or end is None:
|
|
2071
|
+
raise ValueError("rates wants --since and --until as YYYY-MM-DD")
|
|
2072
|
+
if end <= start:
|
|
2073
|
+
raise ValueError("rates wants --until after --since")
|
|
2074
|
+
total = _rate_bucket()
|
|
2075
|
+
days, sessions = {}, 0
|
|
2076
|
+
for path in ledgers():
|
|
2077
|
+
rows = _foreign_rows(path)
|
|
2078
|
+
if fixture_ledger(rows):
|
|
2079
|
+
continue
|
|
2080
|
+
rows = [r for r in rows if isinstance(r.get("ts"), (int, float))
|
|
2081
|
+
and start <= r["ts"] < end]
|
|
2082
|
+
if not rows:
|
|
2083
|
+
continue
|
|
2084
|
+
sessions += 1
|
|
2085
|
+
for entry in rows:
|
|
2086
|
+
_rate_row(total, entry)
|
|
2087
|
+
_rate_row(days.setdefault(_day(entry["ts"]), _rate_bucket()), entry)
|
|
2088
|
+
return dict(total, since=str(since), until=str(until), sessions=sessions,
|
|
2089
|
+
days=int(round((end - start) / 86400.0)),
|
|
2090
|
+
by_day=[dict(bucket, day=day)
|
|
2091
|
+
for day, bucket in sorted(days.items())],
|
|
2092
|
+
rates=_window_rates(total, sessions))
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
def _check_runs(cmd, cwd=None):
|
|
1888
2096
|
"""(name, mode) for every check-shaped command in `cmd`, in order. `mode` is
|
|
1889
2097
|
`check`, `info` (INFO_ARGS / INFO_SUBCOMMAND: it checks nothing) or `write`
|
|
1890
2098
|
(FORMAT_WRITE: it changes the tree), read on the words after the tool up to
|
|
1891
2099
|
the end of that one command, so `pytest --version; pytest` still runs a
|
|
1892
|
-
check.
|
|
2100
|
+
check. With `cwd`, the repository's own declared checks (`repo_checks`)
|
|
2101
|
+
follow the built-in vocabulary's."""
|
|
1893
2102
|
text = mask(cmd)
|
|
1894
2103
|
for m in VERIFY.finditer(text):
|
|
1895
2104
|
name = (m.groupdict().get("js") or m.group(0)).strip()
|
|
1896
2105
|
tool = name.split()[-1].lower()
|
|
1897
2106
|
args = COMMAND_END.split(text[m.end():], 1)[0]
|
|
1898
2107
|
sub = INFO_SUBCOMMAND.get(tool)
|
|
1899
|
-
|
|
2108
|
+
# the info flag may sit among the runner's own flags
|
|
2109
|
+
# (`npm --help test` runs nothing), so the whole match is read too
|
|
2110
|
+
if INFO_ARGS.search(m.group(0)) or INFO_ARGS.search(args) \
|
|
2111
|
+
or (sub and sub.match(args)):
|
|
1900
2112
|
yield name, "info"
|
|
1901
2113
|
elif tool in FORMAT_WRITE and FORMAT_WRITE[tool].search(args):
|
|
1902
2114
|
yield name, "write"
|
|
1903
2115
|
else:
|
|
1904
2116
|
yield name, "check"
|
|
2117
|
+
prefixes = repo_checks(cwd)
|
|
2118
|
+
if prefixes:
|
|
2119
|
+
yield from _repo_check_runs(cmd, prefixes)
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
# The words in front of a program that leave it the program: VERIFY's own
|
|
2123
|
+
# wrapper prefix, read on words instead of text.
|
|
2124
|
+
CHECK_WRAPPERS = frozenset(("sudo", "env", "time", "nohup", "command", "exec"))
|
|
2125
|
+
ASSIGNMENT_WORD = re.compile(r"^[A-Za-z_]\w*=")
|
|
2126
|
+
|
|
2127
|
+
|
|
2128
|
+
def repo_checks(cwd):
|
|
2129
|
+
"""[argv prefix] the repository at `cwd` declares as checks in its manifest
|
|
2130
|
+
(`tezgah_extension.repo_checks`), or [] - no cwd, no manifest, a broken one,
|
|
2131
|
+
or a missing extension module. Imported here, not at the top: a broken
|
|
2132
|
+
extension costs the repository its declared checks, never the hook."""
|
|
2133
|
+
if not cwd:
|
|
2134
|
+
return []
|
|
2135
|
+
try:
|
|
2136
|
+
import tezgah_extension
|
|
2137
|
+
return tezgah_extension.repo_checks(cwd)
|
|
2138
|
+
except Exception:
|
|
2139
|
+
return []
|
|
2140
|
+
|
|
1905
2141
|
|
|
2142
|
+
def _repo_check_runs(cmd, prefixes):
|
|
2143
|
+
"""(name, mode) for each simple command of `cmd` that starts with one of
|
|
2144
|
+
`prefixes`, after assignments and VERIFY's wrappers. Read with
|
|
2145
|
+
`_shell_commands`, so a prefix inside a quoted argument, a commit message or
|
|
2146
|
+
a QUOTED heredoc body (`<<'EOF'`, `<<"EOF"`) is an argument, not a run - the
|
|
2147
|
+
masked reading VERIFY keeps. An unquoted heredoc's body (`<<EOF`, `<<-EOF`)
|
|
2148
|
+
is read as commands, so `cat > x <<EOF` over a `./bin/run-tool` line counts
|
|
2149
|
+
as a run, as a `pytest` line there does for VERIFY.
|
|
2150
|
+
ponytail: a wrapper's own option (`sudo -u root ./bin/x`) hides the program,
|
|
2151
|
+
a lost credit rather than an invented one."""
|
|
2152
|
+
for words, _sep in _shell_commands(cmd):
|
|
2153
|
+
i = 0
|
|
2154
|
+
while i < len(words) and (words[i] in CHECK_WRAPPERS
|
|
2155
|
+
or ASSIGNMENT_WORD.match(words[i])):
|
|
2156
|
+
i += 1
|
|
2157
|
+
run = words[i:]
|
|
2158
|
+
if not run:
|
|
2159
|
+
continue
|
|
2160
|
+
head = [os.path.normpath(run[0])] + run[1:]
|
|
2161
|
+
for prefix in prefixes:
|
|
2162
|
+
if head[:len(prefix)] == prefix:
|
|
2163
|
+
name = " ".join(run[:len(prefix)])
|
|
2164
|
+
rest = " ".join(run[len(prefix):])
|
|
2165
|
+
yield name, ("info" if INFO_ARGS.search(rest) else "check")
|
|
2166
|
+
break
|
|
1906
2167
|
|
|
1907
|
-
|
|
2168
|
+
|
|
2169
|
+
def verify_command(cmd, cwd=None):
|
|
1908
2170
|
"""The name of the check this command runs, or None.
|
|
1909
2171
|
|
|
1910
2172
|
The scan runs on the masked text, the convention `shortcut_command` already
|
|
@@ -1920,8 +2182,9 @@ def verify_command(cmd):
|
|
|
1920
2182
|
(`_check_runs`), so every consumer - the evidence kind, the gate's retry
|
|
1921
2183
|
exemption, its began `check` mark, `piped_check` and the NEUTER rule - reads
|
|
1922
2184
|
them as the plain commands they are. A JS tool names itself, not its runner
|
|
1923
|
-
(`npx axe-core` is `axe-core`).
|
|
1924
|
-
|
|
2185
|
+
(`npx axe-core` is `axe-core`). `cwd` adds the repository's declared checks
|
|
2186
|
+
(`repo_checks`); without it only the built-in vocabulary counts."""
|
|
2187
|
+
return next((name for name, mode in _check_runs(cmd, cwd) if mode == "check"),
|
|
1925
2188
|
None)
|
|
1926
2189
|
|
|
1927
2190
|
|
|
@@ -1994,6 +2257,46 @@ def _closes(cmd, pos, tag, strip, message):
|
|
|
1994
2257
|
return None, n
|
|
1995
2258
|
|
|
1996
2259
|
|
|
2260
|
+
# bash's metacharacters: unquoted, each one ends a word. Any other character -
|
|
2261
|
+
# an escaped or quoted one, a `\r`, a non-breaking space - is part of a word.
|
|
2262
|
+
WORD_BREAKS = " \t\n;&|()<>"
|
|
2263
|
+
# A line continuation: outside single quotes and comments bash deletes both
|
|
2264
|
+
# characters, so the words around it join (`-de\<nl>lete` is `-delete`) and
|
|
2265
|
+
# the word before it goes on (`x\<nl>#` is the word `x#`). After an escaped
|
|
2266
|
+
# backslash (`a\\<nl>`) the newline is a real one and ends the command.
|
|
2267
|
+
CONTINUATION = "\\\n"
|
|
2268
|
+
|
|
2269
|
+
|
|
2270
|
+
def _opens_comment(prev):
|
|
2271
|
+
"""True when an unquoted, unescaped `#` opens a comment: only at the start
|
|
2272
|
+
of a word. `prev` is the last character bash kept before it - "" at the
|
|
2273
|
+
start of the text, a non-break for an escaped or quoted one, and the one
|
|
2274
|
+
before a CONTINUATION, which is no character. The one rule every shell
|
|
2275
|
+
reader asks (`_heredocs`, `_shell_lines`, `mask`, `_shell_read`, and the
|
|
2276
|
+
gate's `_shell_words` through `_shell_lines`), so none of them reads a
|
|
2277
|
+
comment where bash runs a command (review of plan 126, round 3h)."""
|
|
2278
|
+
return not prev or prev in WORD_BREAKS
|
|
2279
|
+
|
|
2280
|
+
|
|
2281
|
+
# What before a `(` makes it part of a word: a command or process substitution
|
|
2282
|
+
# or an arithmetic expansion (`$(`, `<(`, `>(`, `$((`) and an array (`x=(`).
|
|
2283
|
+
WORD_PARENS = "$<>="
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
def _kept(ch, before, parens):
|
|
2287
|
+
"""`ch` as the `prev` `_opens_comment` asks of the character after it: a
|
|
2288
|
+
`)` closing a `(` opened right after WORD_PARENS ends no word (`$(:)#x` is
|
|
2289
|
+
one word, so bash runs a command after it), and reads as "_"; a subshell's,
|
|
2290
|
+
an arithmetic command's or a `case` pattern's `)` is a break. `before` is
|
|
2291
|
+
the character kept before `ch` and `parens` the reader's stack of open `(`
|
|
2292
|
+
(review of plan 126, round 3i)."""
|
|
2293
|
+
if ch == "(":
|
|
2294
|
+
parens.append(bool(before) and before in WORD_PARENS)
|
|
2295
|
+
elif ch == ")" and parens and parens.pop():
|
|
2296
|
+
return "_"
|
|
2297
|
+
return ch
|
|
2298
|
+
|
|
2299
|
+
|
|
1997
2300
|
def _heredocs(cmd):
|
|
1998
2301
|
"""Every heredoc operator in `cmd`, in order, as (operator start, operator
|
|
1999
2302
|
end, tag, quoted, body start, terminator span or None, safe).
|
|
@@ -2015,10 +2318,13 @@ def _heredocs(cmd):
|
|
|
2015
2318
|
bash runs - so the reader no longer decides where such a context ends: it
|
|
2016
2319
|
shows the text. A false refusal of data is the accepted cost
|
|
2017
2320
|
(tests/bash_vectors.ACCEPTED_REFUSALS)."""
|
|
2018
|
-
n, found, pending, i = len(cmd), [], [], 0
|
|
2019
|
-
quote, depth, sure, line_start = None, 0, True, 0
|
|
2321
|
+
n, found, pending, i, parens = len(cmd), [], [], 0, []
|
|
2322
|
+
quote, depth, sure, line_start, last = None, 0, True, 0, ""
|
|
2020
2323
|
while i < n:
|
|
2021
2324
|
ch = cmd[i]
|
|
2325
|
+
# `last` is the character bash kept before `ch`: a quoted or escaped
|
|
2326
|
+
# one leaves "_", a non-break, unless a branch below says otherwise
|
|
2327
|
+
before, last = last, "_"
|
|
2022
2328
|
if quote in ("'", "$'"):
|
|
2023
2329
|
if ch == "\\" and quote == "$'":
|
|
2024
2330
|
i += 2
|
|
@@ -2028,6 +2334,8 @@ def _heredocs(cmd):
|
|
|
2028
2334
|
i += 1
|
|
2029
2335
|
continue
|
|
2030
2336
|
if ch == "\\":
|
|
2337
|
+
if cmd.startswith(CONTINUATION, i):
|
|
2338
|
+
last = before
|
|
2031
2339
|
i += 2
|
|
2032
2340
|
continue
|
|
2033
2341
|
if quote == '"':
|
|
@@ -2039,7 +2347,7 @@ def _heredocs(cmd):
|
|
|
2039
2347
|
continue
|
|
2040
2348
|
if ch == "\n":
|
|
2041
2349
|
if not pending:
|
|
2042
|
-
line_start = i + 1
|
|
2350
|
+
line_start, last = i + 1, ch
|
|
2043
2351
|
i += 1
|
|
2044
2352
|
continue
|
|
2045
2353
|
pos = i + 1
|
|
@@ -2053,7 +2361,7 @@ def _heredocs(cmd):
|
|
|
2053
2361
|
pos = i + 1
|
|
2054
2362
|
pending = []
|
|
2055
2363
|
continue
|
|
2056
|
-
if ch == "#" and (
|
|
2364
|
+
if ch == "#" and _opens_comment(before):
|
|
2057
2365
|
end = cmd.find("\n", i)
|
|
2058
2366
|
i = n if end < 0 else end
|
|
2059
2367
|
continue
|
|
@@ -2078,7 +2386,7 @@ def _heredocs(cmd):
|
|
|
2078
2386
|
i += 1
|
|
2079
2387
|
continue
|
|
2080
2388
|
if cmd.startswith("<<<", i):
|
|
2081
|
-
i
|
|
2389
|
+
i, last = i + 3, "<"
|
|
2082
2390
|
continue
|
|
2083
2391
|
if cmd.startswith("<<", i):
|
|
2084
2392
|
j = i + 2
|
|
@@ -2102,7 +2410,7 @@ def _heredocs(cmd):
|
|
|
2102
2410
|
depth += 1
|
|
2103
2411
|
elif ch in ")]}":
|
|
2104
2412
|
depth -= 1
|
|
2105
|
-
i
|
|
2413
|
+
i, last = i + 1, _kept(ch, before, parens)
|
|
2106
2414
|
found += [(s, e, t, q, n, None, False) for s, e, t, q, _, _ in pending]
|
|
2107
2415
|
return found
|
|
2108
2416
|
|
|
@@ -2151,50 +2459,81 @@ def _heredoc_bytes(text):
|
|
|
2151
2459
|
|
|
2152
2460
|
def mask(text):
|
|
2153
2461
|
"""A shell line with quoted strings, comments and heredoc bodies blanked,
|
|
2154
|
-
length
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
nothing after it - and keeps the reading before it.
|
|
2462
|
+
length kept, read the way bash reads it: `'...'` takes no escape, `$'...'`
|
|
2463
|
+
(an unescaped `$` only) and `"..."` do, a `\\` outside quotes escapes one
|
|
2464
|
+
character, and `#` starts a comment only at the start of a word. So
|
|
2465
|
+
`https://x`, `a#b`, `src/*.py ... lib/*/`, `'x\\'` and `\\$'x\\'` are words,
|
|
2466
|
+
not a comment or an open string that blanks the command after them
|
|
2467
|
+
(gate-01). A quote left open blanks from itself to the end - bash runs
|
|
2468
|
+
nothing after it - and keeps the reading before it. A CONTINUATION bash
|
|
2469
|
+
deletes (outside single quotes, `$'...'` and comments) is moved to its
|
|
2470
|
+
word's start as two spaces, so every rule reads the token bash joins
|
|
2471
|
+
(`tr\\<nl>ue` is `true`, `|\\<nl>|` is `||`) at offsets `joined` keeps
|
|
2472
|
+
(review of plan 126, round 3i).
|
|
2161
2473
|
|
|
2162
2474
|
Memoised per process: a pure function of the text, and one Stop asks it of
|
|
2163
2475
|
the same ledger details from several readers (both Stop folds, plan 063)."""
|
|
2164
|
-
return _mask(str(text or ""))
|
|
2476
|
+
return _mask(str(text or ""))[0]
|
|
2477
|
+
|
|
2478
|
+
|
|
2479
|
+
def joined(text):
|
|
2480
|
+
"""The raw `text` with each CONTINUATION moved as `mask` moves it: the text
|
|
2481
|
+
a rule slices at the offsets a masked match proved."""
|
|
2482
|
+
text = str(text or "")
|
|
2483
|
+
return _moved(list(text), _mask(text)[1])
|
|
2484
|
+
|
|
2485
|
+
|
|
2486
|
+
def _moved(out, cuts):
|
|
2487
|
+
for word, at in cuts:
|
|
2488
|
+
out[word + 2:at + 2] = out[word:at]
|
|
2489
|
+
out[word:word + 2] = " "
|
|
2490
|
+
return "".join(out)
|
|
2165
2491
|
|
|
2166
2492
|
|
|
2167
2493
|
@functools.lru_cache(maxsize=4096)
|
|
2168
2494
|
def _mask(text):
|
|
2495
|
+
"""(the masked text, the (word start, offset) of each CONTINUATION moved)."""
|
|
2169
2496
|
text = _blank_heredocs(text)
|
|
2170
2497
|
body = _heredoc_bytes(text)
|
|
2171
2498
|
out, i, n, start, prev = list(text), 0, len(text), True, ""
|
|
2499
|
+
parens, word, cuts = [], 0, []
|
|
2172
2500
|
while i < n:
|
|
2173
2501
|
ch = text[i]
|
|
2174
2502
|
if body[i]:
|
|
2175
|
-
i, start, prev = i + 1, True, ""
|
|
2503
|
+
i, start, prev, word = i + 1, True, "", i + 1
|
|
2176
2504
|
continue
|
|
2177
2505
|
if ch == "\\":
|
|
2178
|
-
|
|
2506
|
+
# a CONTINUATION is no character: the word state before it goes on
|
|
2507
|
+
if text.startswith(CONTINUATION, i):
|
|
2508
|
+
cuts.append((word, i))
|
|
2509
|
+
else:
|
|
2510
|
+
start, prev = False, ""
|
|
2511
|
+
i += 2
|
|
2179
2512
|
continue
|
|
2180
2513
|
if ch in "'\"":
|
|
2181
2514
|
escapes = ch == '"' or prev == "$"
|
|
2182
2515
|
end = i + 1
|
|
2183
2516
|
while end < n and text[end] != ch:
|
|
2184
|
-
|
|
2517
|
+
if escapes and text[end] == "\\":
|
|
2518
|
+
if ch == '"' and text.startswith(CONTINUATION, end):
|
|
2519
|
+
cuts.append((word, end))
|
|
2520
|
+
end += 1
|
|
2521
|
+
end += 1
|
|
2185
2522
|
end = min(end + 1, n)
|
|
2186
2523
|
elif ch == "#" and start:
|
|
2187
2524
|
end = text.find("\n", i)
|
|
2188
2525
|
end = n if end < 0 else end
|
|
2189
2526
|
else:
|
|
2190
|
-
start, prev =
|
|
2527
|
+
start, prev = _opens_comment(_kept(ch, prev, parens)), ch
|
|
2191
2528
|
i += 1
|
|
2529
|
+
if ch in " \t\n":
|
|
2530
|
+
word = i
|
|
2192
2531
|
continue
|
|
2193
2532
|
for k in range(i, end):
|
|
2194
2533
|
if out[k] != "\n":
|
|
2195
2534
|
out[k] = " "
|
|
2196
2535
|
i, start, prev = end, False, ""
|
|
2197
|
-
return
|
|
2536
|
+
return _moved(out, cuts), tuple(cuts)
|
|
2198
2537
|
|
|
2199
2538
|
|
|
2200
2539
|
def _unquoted_backticks(text):
|
|
@@ -2215,6 +2554,17 @@ def _unquoted_backticks(text):
|
|
|
2215
2554
|
elif ch in "'\"":
|
|
2216
2555
|
quote = ch
|
|
2217
2556
|
elif ch == "`":
|
|
2557
|
+
# a `#` right after it, continuations aside, opens no comment
|
|
2558
|
+
# (`_opens_comment` sees `;`): bash reads it mid-word after the
|
|
2559
|
+
# close, and a comment inside the body ends at the close, so it is
|
|
2560
|
+
# escaped here (review of plan 126, round 3i: `` `:`\<nl># ``)
|
|
2561
|
+
j = i + 1
|
|
2562
|
+
while text.startswith(CONTINUATION, j):
|
|
2563
|
+
j += 2
|
|
2564
|
+
if text.startswith("#", j):
|
|
2565
|
+
out.append(";" + text[i + 1:j] + "\\#")
|
|
2566
|
+
i = j + 1
|
|
2567
|
+
continue
|
|
2218
2568
|
ch = ";"
|
|
2219
2569
|
out.append(ch)
|
|
2220
2570
|
i += 1
|
|
@@ -2257,28 +2607,32 @@ def _for_shlex(text):
|
|
|
2257
2607
|
def _shell_lines(text):
|
|
2258
2608
|
"""`text` split at the newlines that end a command in bash: none inside
|
|
2259
2609
|
quotes (`'...'`, `"..."`, `$'...'`), each one in a heredoc body or its
|
|
2260
|
-
terminator (`_heredoc_bytes`),
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2610
|
+
terminator (`_heredoc_bytes`), a CONTINUATION deleted outside single quotes
|
|
2611
|
+
(in `$'...'` it stays, as bash keeps it), and a comment - `#` where
|
|
2612
|
+
`_opens_comment` - dropped to its line end. A double-quoted string holding
|
|
2613
|
+
a `$( )` or a backtick runs a command, so its newlines still split - the
|
|
2614
|
+
reading this had before, which keeps that command visible. A quote left
|
|
2615
|
+
open at the end goes back to a split at every newline from its line on."""
|
|
2265
2616
|
body = _heredoc_bytes(text)
|
|
2266
|
-
lines, cur, quote,
|
|
2267
|
-
opened, runs = 0, False
|
|
2617
|
+
lines, cur, quote, prev, i, n = [], [], None, "", 0, len(text)
|
|
2618
|
+
opened, runs, parens = 0, False, []
|
|
2268
2619
|
while i < n:
|
|
2269
2620
|
ch = text[i]
|
|
2270
2621
|
if body[i] or (not quote or runs) and ch in "\r\n":
|
|
2271
2622
|
if ch in "\r\n":
|
|
2272
2623
|
lines.append("".join(cur))
|
|
2273
|
-
cur, quote,
|
|
2274
|
-
|
|
2624
|
+
cur, quote, runs = [], None, False
|
|
2625
|
+
step = 2 if text[i:i + 2] == "\r\n" else 1
|
|
2626
|
+
# a lone `\r` is no word break to bash: the split shows more
|
|
2627
|
+
prev, i = text[i + step - 1], i + step
|
|
2275
2628
|
else:
|
|
2276
2629
|
cur.append(ch)
|
|
2277
2630
|
i += 1
|
|
2278
2631
|
continue
|
|
2279
2632
|
if quote:
|
|
2280
2633
|
if ch == "\\" and quote != "'" and i + 1 < n:
|
|
2281
|
-
|
|
2634
|
+
if quote != '"' or not text.startswith(CONTINUATION, i):
|
|
2635
|
+
cur.append(text[i:i + 2])
|
|
2282
2636
|
i += 2
|
|
2283
2637
|
continue
|
|
2284
2638
|
if ch == quote[-1]:
|
|
@@ -2286,17 +2640,19 @@ def _shell_lines(text):
|
|
|
2286
2640
|
elif quote == '"' and (ch == "`" or text.startswith("$(", i)):
|
|
2287
2641
|
runs = True
|
|
2288
2642
|
elif ch == "\\" and i + 1 < n:
|
|
2289
|
-
|
|
2290
|
-
|
|
2643
|
+
if not text.startswith(CONTINUATION, i):
|
|
2644
|
+
cur.append(text[i:i + 2])
|
|
2645
|
+
prev = "_"
|
|
2646
|
+
i += 2
|
|
2291
2647
|
continue
|
|
2292
|
-
elif ch == "#" and
|
|
2648
|
+
elif ch == "#" and _opens_comment(prev):
|
|
2293
2649
|
while i < n and text[i] not in "\r\n":
|
|
2294
2650
|
i += 1
|
|
2295
2651
|
continue
|
|
2296
2652
|
elif ch in "'\"":
|
|
2297
2653
|
quote = "$'" if ch == "'" and cur and cur[-1] == "$" else ch
|
|
2298
2654
|
opened = len(lines)
|
|
2299
|
-
|
|
2655
|
+
prev = "_" if quote or ch in "'\"" else _kept(ch, prev, parens)
|
|
2300
2656
|
cur.append(ch)
|
|
2301
2657
|
i += 1
|
|
2302
2658
|
lines.append("".join(cur))
|
|
@@ -2312,8 +2668,9 @@ def _shell_commands(cmd):
|
|
|
2312
2668
|
A `$( )` or backtick body is a command of its own, ended by `$)`, and the
|
|
2313
2669
|
command around it goes on after its close with a `$()` word in its place."""
|
|
2314
2670
|
out = []
|
|
2315
|
-
text = _blank_heredocs(str(cmd or ""))
|
|
2671
|
+
text = _blank_heredocs(str(cmd or ""))
|
|
2316
2672
|
for line in map(_for_shlex, _shell_lines(text)):
|
|
2673
|
+
line = FD_REDIRECT.sub(r"\2", line)
|
|
2317
2674
|
try:
|
|
2318
2675
|
lex = shlex.shlex(line, posix=True, punctuation_chars=";&|()<>")
|
|
2319
2676
|
lex.whitespace_split = True
|
|
@@ -2328,10 +2685,9 @@ def _shell_commands(cmd):
|
|
|
2328
2685
|
if word == "&" and i < len(words) and REDIRECTION.match(words[i]):
|
|
2329
2686
|
continue # `&>` is a redirection, not a separator
|
|
2330
2687
|
if REDIRECTION.match(word):
|
|
2331
|
-
#
|
|
2332
|
-
#
|
|
2333
|
-
|
|
2334
|
-
cur.pop()
|
|
2688
|
+
# an `&` with its fd (`>&2`), or the target after it - neither is
|
|
2689
|
+
# an argument. The fd number is cut from the line before shlex
|
|
2690
|
+
# (FD_REDIRECT), because `1<` is an fd and `1 <` a program word.
|
|
2335
2691
|
after_redir = True
|
|
2336
2692
|
continue
|
|
2337
2693
|
if after_redir:
|
|
@@ -2339,6 +2695,13 @@ def _shell_commands(cmd):
|
|
|
2339
2695
|
continue # `>&2`: the `&` belongs to the operator
|
|
2340
2696
|
after_redir = False
|
|
2341
2697
|
continue
|
|
2698
|
+
if word.startswith("((") and cur and cur[-1].endswith("$"):
|
|
2699
|
+
# `$(( ))` is arithmetic: its own words are no commands, though
|
|
2700
|
+
# a `$( )` inside one still is and is entered below
|
|
2701
|
+
cur[-1] += "()"
|
|
2702
|
+
subs.append([cur, 1, True])
|
|
2703
|
+
cur = []
|
|
2704
|
+
continue
|
|
2342
2705
|
if not (word and word[0] in ";&|()"):
|
|
2343
2706
|
cur.append(word)
|
|
2344
2707
|
continue
|
|
@@ -2350,21 +2713,32 @@ def _shell_commands(cmd):
|
|
|
2350
2713
|
continue
|
|
2351
2714
|
if subs and piece == "(":
|
|
2352
2715
|
subs[-1][1] += 1
|
|
2716
|
+
if len(subs[-1]) > 2: # a grouping paren inside arithmetic
|
|
2717
|
+
cur = []
|
|
2718
|
+
continue
|
|
2353
2719
|
elif subs and piece == ")":
|
|
2354
2720
|
if not subs[-1][1]:
|
|
2355
|
-
|
|
2721
|
+
arith = len(subs[-1]) > 2
|
|
2722
|
+
if cur and not arith:
|
|
2356
2723
|
out.append([cur, "$)"])
|
|
2357
2724
|
cur = subs.pop()[0]
|
|
2358
2725
|
continue
|
|
2359
2726
|
subs[-1][1] -= 1
|
|
2727
|
+
if len(subs[-1]) > 2: # still the arithmetic's own paren
|
|
2728
|
+
cur = []
|
|
2729
|
+
continue
|
|
2360
2730
|
if cur:
|
|
2361
2731
|
out.append([cur, piece])
|
|
2362
2732
|
elif out:
|
|
2363
2733
|
out[-1][1] += piece
|
|
2364
2734
|
cur = []
|
|
2365
|
-
|
|
2735
|
+
# an arithmetic sub holds no command of its own, only any `$( )` in it,
|
|
2736
|
+
# which was emitted when it closed
|
|
2737
|
+
for seg in [cur] + [sub[0] for sub in reversed(subs) if len(sub) < 3]:
|
|
2366
2738
|
if seg:
|
|
2367
2739
|
out.append([seg, "\n"])
|
|
2740
|
+
elif out: # the line ended on a close: its newline is still a break
|
|
2741
|
+
out[-1][1] += "\n"
|
|
2368
2742
|
if out and out[-1][1] == "\n":
|
|
2369
2743
|
out[-1][1] = ""
|
|
2370
2744
|
return out
|
|
@@ -2374,7 +2748,8 @@ def _shell_segments(cmd):
|
|
|
2374
2748
|
"""The line's simple commands as word lists, read the way
|
|
2375
2749
|
`tezgah_context.shell_programs` reads a line - shlex, posix, punctuation
|
|
2376
2750
|
`;&|()<>` - so quotes and escapes are gone, heredoc bodies are blanked first,
|
|
2377
|
-
a continued line is joined,
|
|
2751
|
+
a continued line is joined (CONTINUATION, in `_shell_lines`, which knows
|
|
2752
|
+
where bash deletes one), a newline ends a command only where bash ends
|
|
2378
2753
|
one (`_shell_lines`), a `$'...'` is one word (`_for_shlex`), and a `$( )` or
|
|
2379
2754
|
backtick body is a command of its own (`_shell_commands`). A line shlex
|
|
2380
2755
|
cannot read is read roughly (`ROUGH_WORDS`) rather than dropped.
|
|
@@ -2639,7 +3014,7 @@ def pipe_hides_status(cmd):
|
|
|
2639
3014
|
OWNING_SEP = re.compile(r"^[()]*(?:&&|\|&?)[()]*$")
|
|
2640
3015
|
|
|
2641
3016
|
|
|
2642
|
-
def status_hidden(cmd):
|
|
3017
|
+
def status_hidden(cmd, cwd=None):
|
|
2643
3018
|
"""True when the line's exit status is not its check's: a pipe owns it
|
|
2644
3019
|
(`pipe_hides_status`), or a command after the check does - the check is
|
|
2645
3020
|
followed by `;` or a newline and more commands, or is sent to the background
|
|
@@ -2648,16 +3023,21 @@ def status_hidden(cmd):
|
|
|
2648
3023
|
status. A heredoc body and its terminator are data here, and an `exit $?`
|
|
2649
3024
|
(or a bare `exit`) right after the check ends the line with the check's
|
|
2650
3025
|
status. Read with `_shell_commands`, each command re-quoted for
|
|
2651
|
-
`verify_command
|
|
2652
|
-
|
|
3026
|
+
`verify_command`; `cwd` reads the repository's declared checks
|
|
3027
|
+
(`repo_checks`) as checks too, so `./bin/run-tool x; echo done` is hidden
|
|
3028
|
+
the way `pytest; echo done` is. ponytail: `set -e` is not read, so `set -e;
|
|
3029
|
+
pytest; echo done` records as ran - a lost credit, never an invented one."""
|
|
2653
3030
|
if pipe_hides_status(cmd):
|
|
2654
3031
|
return True
|
|
2655
3032
|
cmd = str(cmd or "")
|
|
2656
3033
|
body = _heredoc_bytes(cmd)
|
|
2657
3034
|
cmds = _shell_commands("".join(" " if b and ch != "\n" else ch
|
|
2658
3035
|
for ch, b in zip(cmd, body)))
|
|
3036
|
+
prefixes = repo_checks(cwd)
|
|
2659
3037
|
for i, (words, _sep) in enumerate(cmds):
|
|
2660
|
-
|
|
3038
|
+
line = shlex.join(words)
|
|
3039
|
+
if not (verify_command(line) or any(
|
|
3040
|
+
mode == "check" for _n, mode in _repo_check_runs(line, prefixes))):
|
|
2661
3041
|
continue
|
|
2662
3042
|
for k in range(i, len(cmds) - 1):
|
|
2663
3043
|
if OWNING_SEP.match(cmds[k][1]):
|
|
@@ -2685,9 +3065,10 @@ def piped_check(cmd):
|
|
|
2685
3065
|
c = mask(cmd)
|
|
2686
3066
|
if PIPEFAIL.match(c):
|
|
2687
3067
|
return None
|
|
2688
|
-
raw =
|
|
3068
|
+
raw = joined(cmd)
|
|
2689
3069
|
pos, check = 0, None
|
|
2690
|
-
# mask keeps every offset
|
|
3070
|
+
# mask keeps every offset of `joined`, so a stage's span in `c` is its text
|
|
3071
|
+
# in `raw`
|
|
2691
3072
|
for part in re.split(r"(&&|\|\||;|\n|\|)", c):
|
|
2692
3073
|
if part in ("&&", "||", ";", "\n"):
|
|
2693
3074
|
check = None
|
|
@@ -2793,14 +3174,15 @@ def shortcut_edit(inp):
|
|
|
2793
3174
|
return None
|
|
2794
3175
|
|
|
2795
3176
|
|
|
2796
|
-
def classify(tool, inp):
|
|
2797
|
-
"""The evidence kind for a tool call, or None. Shared by the host hooks.
|
|
3177
|
+
def classify(tool, inp, cwd=None):
|
|
3178
|
+
"""The evidence kind for a tool call, or None. Shared by the host hooks.
|
|
3179
|
+
`cwd` lets the repository's declared checks (`repo_checks`) count."""
|
|
2798
3180
|
t = str(tool or "").lower()
|
|
2799
3181
|
if t in WRITE_TOOLS:
|
|
2800
3182
|
return "edit"
|
|
2801
3183
|
if t in BASH_TOOLS:
|
|
2802
3184
|
cmd = inp.get("command") or inp.get("cmd") or ""
|
|
2803
|
-
return "verify" if verify_command(cmd) else "run"
|
|
3185
|
+
return "verify" if verify_command(cmd, cwd) else "run"
|
|
2804
3186
|
return None
|
|
2805
3187
|
|
|
2806
3188
|
|
|
@@ -2847,7 +3229,7 @@ def classify(tool, inp):
|
|
|
2847
3229
|
# host can put in front of the model; the taint notice that flags a later effect
|
|
2848
3230
|
# in such a turn is the untrusted module's own, not the gate's.
|
|
2849
3231
|
WEB_TOOLS = ("web_search", "websearch", "web_fetch", "webfetch", "fetch",
|
|
2850
|
-
"browser", "browse")
|
|
3232
|
+
"browser", "browse", "read_web_page")
|
|
2851
3233
|
MCP_TOOL = re.compile(r"^mcp__", re.I)
|
|
2852
3234
|
# The verb classes of an MCP tool, read off its name: what the server is asked to
|
|
2853
3235
|
# do picks which of the gate's existing content rules read the payload
|
|
@@ -2990,7 +3372,12 @@ TIER_LOCAL_ARGS = ("-h", "--help", "--use")
|
|
|
2990
3372
|
UNTRUSTED_CHANNEL = {"web": "a web result", "mcp": "an MCP server",
|
|
2991
3373
|
"network": "a network read",
|
|
2992
3374
|
"tier": "an external model answer",
|
|
2993
|
-
"subagent": "a subagent's report"
|
|
3375
|
+
"subagent": "a subagent's report",
|
|
3376
|
+
"repo": "a tool this repository declares untrusted"}
|
|
3377
|
+
# The sixth channel is the repository's own: its manifest's `untrusted` list
|
|
3378
|
+
# names programs whose output is someone else's text - a scanner reading a
|
|
3379
|
+
# target, a fetcher this file cannot know (`tezgah_extension.untrusted_programs`).
|
|
3380
|
+
REPO_CHANNEL = "repo"
|
|
2994
3381
|
# A delegate's report is the fifth channel, and the one no host counted: what a
|
|
2995
3382
|
# subagent hands back is prose this session did not write and cannot vouch for,
|
|
2996
3383
|
# read by a parent that never watched it being produced. Spelled the way every
|
|
@@ -3018,14 +3405,35 @@ def _tier_read(cmd):
|
|
|
3018
3405
|
return False
|
|
3019
3406
|
|
|
3020
3407
|
|
|
3021
|
-
def
|
|
3408
|
+
def _repo_untrusted(cmd, cwd):
|
|
3409
|
+
"""True when this shell line runs a program the repository at `cwd` (None:
|
|
3410
|
+
the hook process's directory) declares untrusted. Read at a command
|
|
3411
|
+
position by `shell_programs`, so a mention in an argument is not a run.
|
|
3412
|
+
Imported here for `_tier_read`'s reason; a broken extension, or a process
|
|
3413
|
+
directory that is gone, costs the channel, never the call."""
|
|
3414
|
+
try:
|
|
3415
|
+
import tezgah_extension
|
|
3416
|
+
from tezgah_context import shell_programs
|
|
3417
|
+
names = tezgah_extension.untrusted_programs(cwd or os.getcwd())
|
|
3418
|
+
return bool(names) and bool(names.intersection(shell_programs(cmd)))
|
|
3419
|
+
except Exception:
|
|
3420
|
+
return False
|
|
3421
|
+
|
|
3422
|
+
|
|
3423
|
+
def untrusted_source(tool, inp, cwd=None):
|
|
3022
3424
|
"""The untrusted channel this call's result came through, or None.
|
|
3023
3425
|
|
|
3024
3426
|
Decided from the call, because that is all a PostToolUse hook sees: the
|
|
3025
3427
|
tool's name for the two named channels, the masked command text for a shell
|
|
3026
3428
|
read that left the machine. None means the result is the user's or this
|
|
3027
3429
|
workspace's, which is the normal case - the label names the exception, so it
|
|
3028
|
-
never becomes noise the model learns to skip.
|
|
3430
|
+
never becomes noise the model learns to skip.
|
|
3431
|
+
|
|
3432
|
+
`cwd` is where the repository's `untrusted` declaration is read from; None
|
|
3433
|
+
reads the call's own `cwd`/`workdir`, else the hook process's directory.
|
|
3434
|
+
ponytail: the callers pass no cwd today, so a host that runs its hook
|
|
3435
|
+
elsewhere than the session reads no declaration - a missed label, never a
|
|
3436
|
+
false one."""
|
|
3029
3437
|
name = str(tool or "").strip().lower()
|
|
3030
3438
|
if MCP_TOOL.match(name):
|
|
3031
3439
|
return "mcp"
|
|
@@ -3039,6 +3447,9 @@ def untrusted_source(tool, inp):
|
|
|
3039
3447
|
return "network"
|
|
3040
3448
|
if name in BASH_TOOLS and _tier_read(cmd):
|
|
3041
3449
|
return "tier"
|
|
3450
|
+
if name in BASH_TOOLS and cmd and _repo_untrusted(
|
|
3451
|
+
cmd, cwd or inp.get("cwd") or inp.get("workdir")):
|
|
3452
|
+
return REPO_CHANNEL
|
|
3042
3453
|
return None
|
|
3043
3454
|
|
|
3044
3455
|
|
|
@@ -3380,9 +3791,9 @@ def note_tool(session_id, tool, inp, failed=None, *, interrupted=False,
|
|
|
3380
3791
|
workspace=root_for(cwd) if cwd else None)
|
|
3381
3792
|
return
|
|
3382
3793
|
cmd = str(inp.get("command") or inp.get("cmd") or "")
|
|
3383
|
-
kind = classify(tool, inp)
|
|
3794
|
+
kind = classify(tool, inp, cwd)
|
|
3384
3795
|
if kind == "verify":
|
|
3385
|
-
if failed is None or status_hidden(cmd) or (empty_run and not failed):
|
|
3796
|
+
if failed is None or status_hidden(cmd, cwd) or (empty_run and not failed):
|
|
3386
3797
|
kind = "verify"
|
|
3387
3798
|
else:
|
|
3388
3799
|
kind = "verify_fail" if failed else "verify_ok"
|
|
@@ -3656,26 +4067,67 @@ NEGATION_BEFORE = re.compile(r"(?:%s)(?:\s+\S+){0,2}\s+\Z" % alts("negation_befo
|
|
|
3656
4067
|
QUESTION_PARTICLE = re.compile(r"\s*(?:%s)\b" % alts("question_particle"), re.I)
|
|
3657
4068
|
CLAUSE_END = re.compile(r"[.!?\n,;:\u2014\u2013()]|\b(?:%s)\b" % alts("clause_join"),
|
|
3658
4069
|
re.I)
|
|
4070
|
+
# The first-person claim that a command or a check was run ("test ettim", "i
|
|
4071
|
+
# ran the suite"): the one claim a read-only turn cannot have earned, read by
|
|
4072
|
+
# the `plan claim` class. Narrower than DONE/VERIFIED on purpose - a plan
|
|
4073
|
+
# states its acceptance in those words (plan 142, review round 2).
|
|
4074
|
+
RAN_IT = re.compile(alts("ask", "ran_it"), re.I)
|
|
3659
4075
|
|
|
3660
4076
|
|
|
3661
|
-
def
|
|
3662
|
-
"""
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
rows. ponytail: a window of words, not a parser."""
|
|
4077
|
+
def _asserted(pattern, text):
|
|
4078
|
+
"""The first match of `pattern` in `text` that is neither inside a question
|
|
4079
|
+
nor under a negation of its own clause, else None. ponytail: a window of
|
|
4080
|
+
words, not a parser."""
|
|
3666
4081
|
t = str(text or "")
|
|
4082
|
+
for m in pattern.finditer(t):
|
|
4083
|
+
before = CLAUSE_END.split(t[max(0, m.start() - 80):m.start()])[-1]
|
|
4084
|
+
end = CLAUSE_END.search(t, m.end())
|
|
4085
|
+
asked = (QUESTION_PARTICLE.match(t, m.end())
|
|
4086
|
+
or (end and end.group(0) == "?"))
|
|
4087
|
+
if not (NEGATION_BEFORE.search(before) or asked):
|
|
4088
|
+
return m
|
|
4089
|
+
return None
|
|
3667
4090
|
|
|
3668
|
-
def said(pattern):
|
|
3669
|
-
for m in pattern.finditer(t):
|
|
3670
|
-
before = CLAUSE_END.split(t[max(0, m.start() - 80):m.start()])[-1]
|
|
3671
|
-
end = CLAUSE_END.search(t, m.end())
|
|
3672
|
-
asked = (QUESTION_PARTICLE.match(t, m.end())
|
|
3673
|
-
or (end and end.group(0) == "?"))
|
|
3674
|
-
if not (NEGATION_BEFORE.search(before) or asked):
|
|
3675
|
-
return True
|
|
3676
|
-
return False
|
|
3677
4091
|
|
|
3678
|
-
|
|
4092
|
+
def asserted_claims(text):
|
|
4093
|
+
"""DONE/VERIFIED, with a claim word inside a question or under a negation of
|
|
4094
|
+
its own clause not counted (`_asserted`). Read by `_stop_block` on the
|
|
4095
|
+
no-work path and by `stop_reason` for the claim row; a turn that did work is
|
|
4096
|
+
still judged on its rows."""
|
|
4097
|
+
return bool(_asserted(DONE, text)), bool(_asserted(VERIFIED, text))
|
|
4098
|
+
|
|
4099
|
+
|
|
4100
|
+
def reply_prose(value):
|
|
4101
|
+
"""The reply's prose, whatever the host handed the Stop rule (plan 143).
|
|
4102
|
+
|
|
4103
|
+
omp's `session_stop` carries `last_assistant_message` as the message
|
|
4104
|
+
OBJECT its `getLastAssistantMessage()` returns, not as text, and the bridge
|
|
4105
|
+
passed it through: the rule then judged `str(dict)` - one line, no list
|
|
4106
|
+
items, the prose diluted by the object's own fields. Measured on the
|
|
4107
|
+
owner's ledger, 1643 of 1656 omp `shape` rows of 45 days carried
|
|
4108
|
+
`lines: 1`, against 209 of 231 multi-line on every other host, so every
|
|
4109
|
+
line-shaped class (the list cap, the residual sections, the question the
|
|
4110
|
+
reply ends on, the handed step) read a text that had no lines.
|
|
4111
|
+
|
|
4112
|
+
A string passes through. A message object yields its `text` blocks, joined
|
|
4113
|
+
the way the model wrote them; a thinking or tool block is not the reply.
|
|
4114
|
+
Anything else is no reply at all, which is what the rule already does with
|
|
4115
|
+
a missing one: it never becomes a judgement on a repr.
|
|
4116
|
+
"""
|
|
4117
|
+
if isinstance(value, str):
|
|
4118
|
+
return value
|
|
4119
|
+
if isinstance(value, dict):
|
|
4120
|
+
blocks = value.get("content")
|
|
4121
|
+
if isinstance(blocks, str):
|
|
4122
|
+
return blocks
|
|
4123
|
+
if isinstance(blocks, list):
|
|
4124
|
+
return "\n".join(
|
|
4125
|
+
block["text"] for block in blocks
|
|
4126
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
4127
|
+
and isinstance(block.get("text"), str)).strip()
|
|
4128
|
+
if isinstance(value.get("text"), str):
|
|
4129
|
+
return value["text"]
|
|
4130
|
+
return ""
|
|
3679
4131
|
|
|
3680
4132
|
|
|
3681
4133
|
def passing_check(entry):
|
|
@@ -3692,7 +4144,15 @@ def passing_check(entry):
|
|
|
3692
4144
|
return False
|
|
3693
4145
|
if entry.get(ORPHAN) or entry.get("exit") != 0 or entry.get("out_bytes") == 0:
|
|
3694
4146
|
return False
|
|
3695
|
-
|
|
4147
|
+
# The row carries no cwd: its one location is `repo`, which `_check_repo`
|
|
4148
|
+
# sets only when the call named where it ran (a `cwd`/`workdir` argument, a
|
|
4149
|
+
# leading `cd X`, path arguments in one repository) and leaves None
|
|
4150
|
+
# otherwise. So this re-read sees the repository's declared checks
|
|
4151
|
+
# (`repo_checks`) only on such a row; on the rest it reads the built-in
|
|
4152
|
+
# vocabulary alone. The reading that holds `./bin/run-tool x; echo done` to
|
|
4153
|
+
# `verify` is `note_tool`'s, with the call's own cwd - this one guards a
|
|
4154
|
+
# `verify_ok` row another writer appended, as far as its `repo` reaches.
|
|
4155
|
+
return not status_hidden(entry.get("detail"), entry.get("repo"))
|
|
3696
4156
|
|
|
3697
4157
|
|
|
3698
4158
|
def _changed_write(row):
|
|
@@ -3949,7 +4409,26 @@ def _ui_evidence(rows):
|
|
|
3949
4409
|
return write, proof
|
|
3950
4410
|
|
|
3951
4411
|
|
|
3952
|
-
def
|
|
4412
|
+
def _slop_row_holds(row, cwd):
|
|
4413
|
+
"""True when a `tezgah-design slop` row is evidence for THIS repository.
|
|
4414
|
+
|
|
4415
|
+
The verdict is a file, so the command alone is not enough: a scan pointed at
|
|
4416
|
+
another tree (`--repo <fixture>`) would pass a component edit it never looked
|
|
4417
|
+
at (review of plan 133, finding 5). The repo the row names, or the row's own
|
|
4418
|
+
cwd when it names none, has to be the session's repository."""
|
|
4419
|
+
detail = mask(str(row.get("detail") or ""))
|
|
4420
|
+
named = SLOP_REPO.search(detail)
|
|
4421
|
+
target = named.group(1).strip("'\"") if named else ""
|
|
4422
|
+
base = os.path.realpath(os.path.abspath(cwd or os.getcwd()))
|
|
4423
|
+
root = _toplevel(base) or base
|
|
4424
|
+
want = os.path.realpath(root)
|
|
4425
|
+
if not target or target in (".", "./"):
|
|
4426
|
+
return True
|
|
4427
|
+
path = os.path.realpath(os.path.join(root, os.path.expanduser(target)))
|
|
4428
|
+
return path == want or path.startswith(want + os.sep)
|
|
4429
|
+
|
|
4430
|
+
|
|
4431
|
+
def _design_evidence(rows, cwd=None):
|
|
3953
4432
|
"""(component, check) as row indices for the design-contract half of the UI
|
|
3954
4433
|
rule, -1 for either when the row is not there.
|
|
3955
4434
|
|
|
@@ -3966,6 +4445,8 @@ def _design_evidence(rows):
|
|
|
3966
4445
|
if _ui_write(row) and DESIGN_COMPONENT.search(detail):
|
|
3967
4446
|
component = i
|
|
3968
4447
|
if passing_check(row) and DESIGN_CHECK.search(detail):
|
|
4448
|
+
if DESIGN_SLOP.search(detail) and not _slop_row_holds(row, cwd):
|
|
4449
|
+
continue
|
|
3969
4450
|
check = i
|
|
3970
4451
|
return component, check
|
|
3971
4452
|
|
|
@@ -4409,10 +4890,14 @@ def _record_only(refused, turns, after_block, cls):
|
|
|
4409
4890
|
|
|
4410
4891
|
|
|
4411
4892
|
def stop_reason(text, session_id, cwd=None, after_block=0,
|
|
4412
|
-
subagent=False, agent=None, parent=None):
|
|
4893
|
+
subagent=False, agent=None, parent=None, mode=None):
|
|
4413
4894
|
"""Why this turn must not end yet, or None. Used by the Stop hooks (Claude,
|
|
4414
4895
|
Codex, omp and Cursor, which share the payload fields and the block envelope).
|
|
4415
4896
|
`cwd` is the session's directory, read for the repo's `.no-adhd` mark.
|
|
4897
|
+
`mode` is the host's permission mode for this turn (Claude, Command Code
|
|
4898
|
+
and Qwen send `permission_mode`; omp's extension sends its plan state as
|
|
4899
|
+
one): a read-only one (`read_only_turn`) exempts the approval question the
|
|
4900
|
+
mode asks for and refuses the done/tested claim it cannot have earned.
|
|
4416
4901
|
|
|
4417
4902
|
The verdict is read off the current turn's rows (`turn_rows`) and not the
|
|
4418
4903
|
whole ledger: the file grows with the session, the handler runs once per
|
|
@@ -4458,15 +4943,19 @@ def stop_reason(text, session_id, cwd=None, after_block=0,
|
|
|
4458
4943
|
the child keeps a ledger of its own (omp's task sessions); the child's own
|
|
4459
4944
|
rows still judge it. Both return None. A reply counts as
|
|
4460
4945
|
claiming only with `asserted_claims`: "Testler geçti mi?" leaves no
|
|
4461
|
-
`claim ok` row.
|
|
4946
|
+
`claim ok` row. `text` is read through `reply_prose`, so a host that hands
|
|
4947
|
+
over a message object rather than its text is judged on the prose (plan
|
|
4948
|
+
143)."""
|
|
4949
|
+
text = reply_prose(text)
|
|
4462
4950
|
rows, turns = turn_rows(session_id, turns=True)
|
|
4463
4951
|
chain, rows = (_turn_refusals(session_id, rows) if turns
|
|
4464
4952
|
else (list(filter(_blocked, rows)), rows))
|
|
4465
4953
|
refused = [str(entry.get("detail"))[len("blocked: "):] for entry in chain]
|
|
4466
4954
|
if after_block and not subagent and not refused:
|
|
4467
4955
|
return None
|
|
4956
|
+
read_only = read_only_turn(mode)
|
|
4468
4957
|
cls, reason = _stop_block(text, session_id, rows=rows, cwd=cwd,
|
|
4469
|
-
shape=not subagent)
|
|
4958
|
+
shape=not subagent, read_only=read_only)
|
|
4470
4959
|
record_only = subagent or bool(
|
|
4471
4960
|
(refused if turns else after_block)
|
|
4472
4961
|
and (not reason or _record_only(refused, turns, after_block, cls)))
|
|
@@ -4474,7 +4963,11 @@ def stop_reason(text, session_id, cwd=None, after_block=0,
|
|
|
4474
4963
|
# Keyed like the claim row, so Cursor's re-run of the handler on a follow-up
|
|
4475
4964
|
# does not count the same reply twice; above the early return, so a reply
|
|
4476
4965
|
# with no claim vocabulary still leaves its row.
|
|
4966
|
+
# `mode` rides every row of a read-only turn: the verdict depended on it,
|
|
4967
|
+
# and a replay of the ledger reads it back (`tezgah_replay._replay_one`).
|
|
4477
4968
|
shape = reply_shape(text)
|
|
4969
|
+
if read_only:
|
|
4970
|
+
shape = dict(shape, mode=str(mode).strip().lower())
|
|
4478
4971
|
if not record_only and not any(entry.get("kind") == "shape"
|
|
4479
4972
|
and entry.get("id") == key for entry in rows):
|
|
4480
4973
|
note(session_id, "shape", ",".join(shape_flags(text)) or "ok", id=key,
|
|
@@ -4578,6 +5071,12 @@ GIT_BOOKKEEPING = frozenset((
|
|
|
4578
5071
|
# is work - an unknown program owes the check, a known one never excuses a write.
|
|
4579
5072
|
GIT_READS = GIT_BOOKKEEPING - {"add", "commit", "push", "fetch", "branch", "tag",
|
|
4580
5073
|
"remote"}
|
|
5074
|
+
# `set`'s flags that only change when the shell stops or what it traces: the
|
|
5075
|
+
# `set -o pipefail;` prefix the contract asks a check line for. Anything else
|
|
5076
|
+
# is no read: `-k` and `+o interactive-comments` change how the rest of the
|
|
5077
|
+
# line runs, a plain word sets the positional parameters.
|
|
5078
|
+
SET_FLAGS = re.compile(r"[-+][euxvo]+")
|
|
5079
|
+
SET_NAMES = frozenset(("pipefail", "errexit", "nounset", "xtrace", "verbose"))
|
|
4581
5080
|
# ssh's options that take the next word, so the host is read after them
|
|
4582
5081
|
SSH_VALUE_OPTS = frozenset(("-b", "-c", "-D", "-E", "-e", "-F", "-I", "-i", "-J",
|
|
4583
5082
|
"-L", "-l", "-m", "-O", "-o", "-p", "-Q", "-R", "-S",
|
|
@@ -4607,6 +5106,7 @@ SUBCOMMAND_READS = {"gh": (("pr", "view"), ("pr", "list"), ("issue", "view"),
|
|
|
4607
5106
|
# all (review F6), as a pattern read at an argument's start: git's `--output`
|
|
4608
5107
|
# and external diff/textconv drivers, ripgrep's `--pre`, find's actions, a
|
|
4609
5108
|
# short-option cluster holding sort's `-o`, sed's `-i`/`-f`, awk's `-i`/`-f`,
|
|
5109
|
+
# printf's `-v` (it assigns a variable, PATH included: plan 126),
|
|
4610
5110
|
# and curl's writes to a file and non-GET requests. `tree` and `file` left the
|
|
4611
5111
|
# list instead of growing one here: tree writes through clustered short options
|
|
4612
5112
|
# (`-fo out`) and `-R -H`, file through `-C` (review N4).
|
|
@@ -4615,6 +5115,7 @@ WRITING_OPTIONS = {
|
|
|
4615
5115
|
"rg": re.compile(r"--pre"),
|
|
4616
5116
|
"find": re.compile(r"-(?:delete|exec|execdir|ok|okdir|fprint0?|fprintf|fls)$"),
|
|
4617
5117
|
"sort": re.compile(r"-[^-]*o|--output"),
|
|
5118
|
+
"printf": re.compile(r"-v"),
|
|
4618
5119
|
"sed": re.compile(r"-[^-]*[if]|--(?:in-place|file)"),
|
|
4619
5120
|
"awk": re.compile(r"-[^-]*[if]|--(?:include|file|load)"),
|
|
4620
5121
|
"gawk": re.compile(r"-[^-]*[if]|--(?:include|file|load|inplace)"),
|
|
@@ -4626,9 +5127,32 @@ WRITING_OPTIONS = {
|
|
|
4626
5127
|
GIT_CONFIG_OPTS = ("-c", "--config-env", "--exec-path")
|
|
4627
5128
|
# an fd duplication or a discard: a redirect that writes no file
|
|
4628
5129
|
HARMLESS_REDIRECT = re.compile(r"\d*>&\d+|\d*>>?\s*/dev/null\b")
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
5130
|
+
# The compound-command words `loops=True` reads (plan 126): an opener is no
|
|
5131
|
+
# program (the command after it is: `do cat "$f"`), a closer runs nothing
|
|
5132
|
+
SHELL_OPENERS = frozenset(("if", "then", "elif", "else", "do", "while", "until",
|
|
5133
|
+
"!", "{"))
|
|
5134
|
+
SHELL_CLOSERS = frozenset(("done", "fi", "}"))
|
|
5135
|
+
# a loop variable no shell reads: one lowercase letter. A longer lowercase
|
|
5136
|
+
# name can be one zsh ties to a setting (`path` is PATH, and `cdpath`, `fpath`,
|
|
5137
|
+
# `manpath`, `module_path`, `psvar`, `watch`, `histchars`, `prompt`, `status`
|
|
5138
|
+
# and the modules' `options`, `mapfile`... are its own), and its list grows
|
|
5139
|
+
# with every loaded module
|
|
5140
|
+
LOOP_NAME = re.compile(r"[a-z]")
|
|
5141
|
+
# A word this reader passes on: bash expands neither a brace list nor an
|
|
5142
|
+
# operator form to anything it can read. `{-delete,-print}` is two words,
|
|
5143
|
+
# `${x:--delete}` an option, `$[x]`, `$((x))` and `${a[x]}` evaluate x as
|
|
5144
|
+
# arithmetic, which runs a `$( )` in its value. Read on `_shell_read`'s masks.
|
|
5145
|
+
BRACE_EXPANSION = re.compile(r"\{[^\s;|&<>()]*(?:,|\.\.)[^\s;|&<>()]*\}")
|
|
5146
|
+
PLAIN_DOLLAR = re.compile(r"\$(?:[A-Za-z_]\w*|\{(?:[A-Za-z_]\w*|[0-9@*#?$!-])\}|"
|
|
5147
|
+
r"[0-9@*#?$!-]|(?![\[{(]))")
|
|
5148
|
+
# a function's arguments: values this reader never sees
|
|
5149
|
+
POSITIONAL = re.compile(r"\$\{?[0-9@*#]")
|
|
5150
|
+
# the readers no option of which writes or runs: the only ones a value this
|
|
5151
|
+
# reader never sees (a loop word, a substitution's output) may reach
|
|
5152
|
+
OPTION_FREE = READ_ONLY_PROGRAMS - set(WRITING_OPTIONS)
|
|
5153
|
+
|
|
5154
|
+
|
|
5155
|
+
def _shell_read(cmd, subst=False):
|
|
4632
5156
|
"""(effect, text) for `cmd`: whether bash would run a command or process
|
|
4633
5157
|
substitution, or redirect into a file, anywhere in it - or this reader
|
|
4634
5158
|
cannot tell - and the command with what is not a command blanked for
|
|
@@ -4645,7 +5169,16 @@ def _shell_read(cmd):
|
|
|
4645
5169
|
|
|
4646
5170
|
Fails closed (effect True): an unquoted heredoc expands `$(` in its body,
|
|
4647
5171
|
an unterminated one or an empty delimiter cannot be read, and a quote still
|
|
4648
|
-
open at the end means the reading lost sync with bash (review N1).
|
|
5172
|
+
open at the end means the reading lost sync with bash (review N1).
|
|
5173
|
+
|
|
5174
|
+
Also an effect: a brace expansion (unquoted `{a,b}`), a function definition
|
|
5175
|
+
(`name()`: its body runs with arguments read nowhere), and a word whose `$`
|
|
5176
|
+
is more than a plain `$name`/`${name}` or holds a `[` (`PLAIN_DOLLAR`;
|
|
5177
|
+
double quotes still expand it, single quotes do not).
|
|
5178
|
+
|
|
5179
|
+
`subst=True` reads an unquoted `$(` as no effect of its own: its body is a
|
|
5180
|
+
command `_shell_segments` returns, and its redirects are in the bare text;
|
|
5181
|
+
inside double quotes it stays one word that reader never splits."""
|
|
4649
5182
|
n, out = len(cmd), list(cmd)
|
|
4650
5183
|
|
|
4651
5184
|
def blank(a, b):
|
|
@@ -4659,7 +5192,8 @@ def _shell_read(cmd):
|
|
|
4659
5192
|
blank(start, stop)
|
|
4660
5193
|
blank(body, term[1])
|
|
4661
5194
|
cmd = "".join(out)
|
|
4662
|
-
bare, quote, i = [], None, 0
|
|
5195
|
+
bare, lit, dq, quote, i = [], [], [], None, 0
|
|
5196
|
+
kept, parens = "", [] # lit's last character as `_opens_comment` asks it
|
|
4663
5197
|
while i < n:
|
|
4664
5198
|
ch = cmd[i]
|
|
4665
5199
|
if quote == "'":
|
|
@@ -4670,14 +5204,25 @@ def _shell_read(cmd):
|
|
|
4670
5204
|
elif ch == "'":
|
|
4671
5205
|
quote = None
|
|
4672
5206
|
elif ch == "\\":
|
|
5207
|
+
# a CONTINUATION: bash deletes both, so `$\<nl>1` is `$1` and
|
|
5208
|
+
# nothing is left in the word
|
|
5209
|
+
if cmd.startswith(CONTINUATION, i):
|
|
5210
|
+
i += 2
|
|
5211
|
+
continue
|
|
4673
5212
|
i += 2
|
|
4674
5213
|
bare.append(" ")
|
|
5214
|
+
lit.append("_")
|
|
5215
|
+
dq.append("_")
|
|
5216
|
+
kept = "_"
|
|
4675
5217
|
continue
|
|
4676
|
-
elif ch == "`" or cmd.startswith("$(", i):
|
|
5218
|
+
elif ch == "`" or (cmd.startswith("$(", i) and (quote or not subst)):
|
|
4677
5219
|
return True, ""
|
|
4678
5220
|
elif quote == '"':
|
|
4679
5221
|
quote = None if ch == '"' else quote
|
|
4680
|
-
|
|
5222
|
+
# a `#` opens a comment only at a word's start (`_opens_comment`): after
|
|
5223
|
+
# an escaped or quoted character (`x\ #...`) or a substitution's `)`
|
|
5224
|
+
# (`_kept`) it is part of the word
|
|
5225
|
+
elif ch == "#" and _opens_comment(kept):
|
|
4681
5226
|
end = cmd.find("\n", i)
|
|
4682
5227
|
end = n if end < 0 else end
|
|
4683
5228
|
blank(i, end)
|
|
@@ -4690,19 +5235,53 @@ def _shell_read(cmd):
|
|
|
4690
5235
|
quote = ch
|
|
4691
5236
|
else:
|
|
4692
5237
|
bare.append(ch)
|
|
5238
|
+
lit.append(ch)
|
|
5239
|
+
dq.append(ch)
|
|
5240
|
+
kept = _kept(ch, kept, parens)
|
|
4693
5241
|
i += 1
|
|
4694
5242
|
continue
|
|
4695
5243
|
bare.append(" ")
|
|
5244
|
+
# `lit` keeps only unquoted text, `dq` double-quoted text too
|
|
5245
|
+
lit.append("_")
|
|
5246
|
+
kept = "_"
|
|
5247
|
+
dq.append(ch if quote == '"' or ch == '"' and not quote else "_")
|
|
4696
5248
|
i += 1
|
|
4697
5249
|
if quote:
|
|
4698
5250
|
return True, ""
|
|
5251
|
+
lit, dq = "".join(lit), "".join(dq)
|
|
5252
|
+
if BRACE_EXPANSION.search(lit) or re.search(r"\(\s*\)", lit) or "$((" in dq:
|
|
5253
|
+
return True, ""
|
|
5254
|
+
for word in re.split(r"[\s;&|()<>]+", dq):
|
|
5255
|
+
if "$" in word and ("[" in word or "$" in PLAIN_DOLLAR.sub("", word)):
|
|
5256
|
+
return True, ""
|
|
5257
|
+
# a positional parameter is a value no read here sees: where loops read
|
|
5258
|
+
# (`subst`), a `$1` the shell expands - unquoted or double-quoted, never one
|
|
5259
|
+
# inside single quotes, a comment or a heredoc body - makes the command work
|
|
5260
|
+
if subst and POSITIONAL.search(dq):
|
|
5261
|
+
return True, ""
|
|
4699
5262
|
text = HARMLESS_REDIRECT.sub(" ", "".join(bare))
|
|
4700
5263
|
# every `<` left is an input redirect or a here-string, both reads; blanking
|
|
4701
5264
|
# them keeps `_shell_segments`' own heredoc pass from reading one again
|
|
4702
5265
|
return ">" in text or "<(" in text, "".join(out).replace("<", " ")
|
|
4703
5266
|
|
|
4704
5267
|
|
|
4705
|
-
def
|
|
5268
|
+
def _set_options(args):
|
|
5269
|
+
"""True when `set`'s words are only SET_FLAGS, each `o` in them followed by
|
|
5270
|
+
a SET_NAMES name or by nothing (`set -o` prints the options). A bare `set`
|
|
5271
|
+
prints every variable, and stays work like any other unlisted shape."""
|
|
5272
|
+
j = 0
|
|
5273
|
+
while j < len(args):
|
|
5274
|
+
if not SET_FLAGS.fullmatch(args[j]):
|
|
5275
|
+
return False
|
|
5276
|
+
for _ in range(args[j].count("o")):
|
|
5277
|
+
j += 1
|
|
5278
|
+
if j < len(args) and args[j] not in SET_NAMES:
|
|
5279
|
+
return False
|
|
5280
|
+
j += 1
|
|
5281
|
+
return bool(args)
|
|
5282
|
+
|
|
5283
|
+
|
|
5284
|
+
def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING, loops=False):
|
|
4706
5285
|
"""True when every simple command of `cmd` reads, or runs a git subcommand
|
|
4707
5286
|
in `git` (VCS bookkeeping that changes no working-tree file by default,
|
|
4708
5287
|
GIT_READS for a pure read). A redirect, a substitution, an env assignment
|
|
@@ -4713,21 +5292,40 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4713
5292
|
`codegraph` with a read verb (`SUBCOMMAND_READS`). `sqlite3` reads with a
|
|
4714
5293
|
read-only open (the option, or `mode=ro` in the database URI) and no
|
|
4715
5294
|
writing dot-command or stdin (`SQLITE_WRITE`), `ssh` with a remote command
|
|
4716
|
-
that is itself one of these reads
|
|
5295
|
+
that is itself one of these reads, `set` with shell options only
|
|
5296
|
+
(`_set_options`).
|
|
5297
|
+
|
|
5298
|
+
`loops=True` (plan 126) also reads a `for NAME in WORDS` loop, the
|
|
5299
|
+
`if`/`while`/`{` openers and closers around reads, and an unquoted `$( )`
|
|
5300
|
+
whose body reads. The values those bind - a loop word, a substitution's
|
|
5301
|
+
output, a positional parameter (`POSITIONAL`) - are text this reader never
|
|
5302
|
+
sees, so in such a command a `$` word may only reach a reader no option of
|
|
5303
|
+
which writes (`OPTION_FREE`), and the loop variable is one letter no shell
|
|
5304
|
+
reads (`LOOP_NAME`).
|
|
4717
5305
|
|
|
4718
5306
|
ponytail: `git commit` and `git push` run the repository's hooks, and a
|
|
4719
5307
|
pre-commit autofixer can rewrite files the row never names. They stay on
|
|
4720
5308
|
the list because refusing every commit was the measured cost (audit CHAT-04);
|
|
4721
5309
|
a hook that rewrites the tree is invisible here."""
|
|
4722
5310
|
raw = str(cmd or "").replace(FAILED_MARK, "")
|
|
4723
|
-
effect, text = _shell_read(raw) if raw.strip() else (True, "")
|
|
5311
|
+
effect, text = _shell_read(raw, loops) if raw.strip() else (True, "")
|
|
4724
5312
|
if effect:
|
|
4725
5313
|
return False
|
|
4726
5314
|
segs = _shell_segments(text)
|
|
5315
|
+
unseen = loops and any("for" in w or any("$(" in a for a in w) for w in segs)
|
|
4727
5316
|
for words in segs:
|
|
4728
5317
|
i = 0
|
|
4729
|
-
while i < len(words) and words[i] in GIT_WRAPPER
|
|
5318
|
+
while i < len(words) and (words[i] in GIT_WRAPPER
|
|
5319
|
+
or loops and words[i] in SHELL_OPENERS):
|
|
4730
5320
|
i += 1
|
|
5321
|
+
if loops and all(w in SHELL_OPENERS for w in words[:i]) and (
|
|
5322
|
+
i == len(words) or words[i:] == words[-1:] and words[i] in SHELL_CLOSERS):
|
|
5323
|
+
continue
|
|
5324
|
+
if loops and words[i:i + 1] == ["for"]:
|
|
5325
|
+
if not (words[i + 1:] and LOOP_NAME.fullmatch(words[i + 1])
|
|
5326
|
+
and words[i + 2:i + 3] in ([], ["in"])):
|
|
5327
|
+
return False
|
|
5328
|
+
continue
|
|
4731
5329
|
if i >= len(words) or ENV_WORD.match(words[i]):
|
|
4732
5330
|
return False
|
|
4733
5331
|
# a program named by a path is whatever that file is, not the
|
|
@@ -4735,6 +5333,8 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4735
5333
|
if "/" in words[i]:
|
|
4736
5334
|
return False
|
|
4737
5335
|
program, args = words[i], words[i + 1:]
|
|
5336
|
+
if unseen and program not in OPTION_FREE and any("$" in a for a in args):
|
|
5337
|
+
return False
|
|
4738
5338
|
writes = WRITING_OPTIONS.get(program)
|
|
4739
5339
|
if writes and any(writes.match(a) for a in args):
|
|
4740
5340
|
return False
|
|
@@ -4745,6 +5345,10 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4745
5345
|
if any(SCRIPT_WRITE[program].search(a) for a in plain):
|
|
4746
5346
|
return False
|
|
4747
5347
|
continue
|
|
5348
|
+
if program == "set":
|
|
5349
|
+
if not _set_options(args):
|
|
5350
|
+
return False
|
|
5351
|
+
continue
|
|
4748
5352
|
if program == "uniq":
|
|
4749
5353
|
# `uniq IN OUT` writes its second operand
|
|
4750
5354
|
if len(plain) > 1:
|
|
@@ -4786,7 +5390,7 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4786
5390
|
return bool(segs)
|
|
4787
5391
|
|
|
4788
5392
|
|
|
4789
|
-
def _bookkeeping_turn(rows, git=GIT_BOOKKEEPING):
|
|
5393
|
+
def _bookkeeping_turn(rows, git=GIT_BOOKKEEPING, loops=False):
|
|
4790
5394
|
"""True when every work row in `rows` is a shell call that read or ran a
|
|
4791
5395
|
git subcommand in `git` (`_bookkeeping_command`) and was not seen to
|
|
4792
5396
|
change a file.
|
|
@@ -4800,7 +5404,7 @@ def _bookkeeping_turn(rows, git=GIT_BOOKKEEPING):
|
|
|
4800
5404
|
r.get("kind") == "run" and not _change_row(r)
|
|
4801
5405
|
and len(str(r.get("detail") or "")) < DETAIL_MAX
|
|
4802
5406
|
and "[redacted:" not in str(r.get("detail") or "")
|
|
4803
|
-
and _bookkeeping_command(r.get("detail"), git) for r in steps)
|
|
5407
|
+
and _bookkeeping_command(r.get("detail"), git, loops) for r in steps)
|
|
4804
5408
|
|
|
4805
5409
|
|
|
4806
5410
|
def _settled(rows):
|
|
@@ -4814,7 +5418,8 @@ def _settled(rows):
|
|
|
4814
5418
|
return not after or _bookkeeping_turn(after)
|
|
4815
5419
|
|
|
4816
5420
|
|
|
4817
|
-
def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None
|
|
5421
|
+
def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None,
|
|
5422
|
+
read_only=False):
|
|
4818
5423
|
"""stop_reason's decision as (reason class, block text), without the ledger
|
|
4819
5424
|
side effect. The class names the branch that refused the turn; the text is
|
|
4820
5425
|
what the host shows the model.
|
|
@@ -4871,11 +5476,18 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4871
5476
|
`fold` is the evidence fold the selector below calls. None, the default,
|
|
4872
5477
|
is the temporal spec's (`tezgah_stopspec.fold`: the class from its formulas,
|
|
4873
5478
|
the text from `_evidence_text`), and that outer call fills the text's
|
|
4874
|
-
placeholders once (`_said`).
|
|
5479
|
+
placeholders once (`_said`).
|
|
5480
|
+
|
|
5481
|
+
`read_only` is the host's word that this turn ran in a plan mode
|
|
5482
|
+
(`read_only_turn`), where no command and no write was possible. Two classes
|
|
5483
|
+
read it: `plan claim` below, which refuses a done/tested claim such a turn
|
|
5484
|
+
cannot have behind it, and the `needless ask` exemption in
|
|
5485
|
+
`_residual_block`, since the approval question is what the mode asks for."""
|
|
4875
5486
|
if fold is None:
|
|
4876
5487
|
# imported here: tezgah_stopspec imports this module at its top
|
|
4877
5488
|
import tezgah_stopspec
|
|
4878
|
-
verdict = _stop_block(text, session_id, rows, cwd, shape,
|
|
5489
|
+
verdict = _stop_block(text, session_id, rows, cwd, shape,
|
|
5490
|
+
tezgah_stopspec.fold, read_only)
|
|
4879
5491
|
return (verdict[0], verdict[1] and _said(verdict[1]))
|
|
4880
5492
|
t = str(text or "")
|
|
4881
5493
|
shaped = _shape_block(t, cwd) if shape else (None, None)
|
|
@@ -4889,6 +5501,19 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4889
5501
|
return ("evidence tampered", TAMPERED)
|
|
4890
5502
|
if (done or verified) and any(r.get(ORPHAN) for r in rows):
|
|
4891
5503
|
return ("evidence tampered", ORPHANED)
|
|
5504
|
+
# A read-only turn (the host's plan mode) ran no check, so a reply that says
|
|
5505
|
+
# it ran one says what cannot have happened - unless a check of its own
|
|
5506
|
+
# passed, which is evidence like any other. The trigger is the first-person
|
|
5507
|
+
# run claim (RAN_IT: "test ettim", "i ran the suite"), never the done or
|
|
5508
|
+
# verified vocabulary: a plan states its acceptance in those words ("Done
|
|
5509
|
+
# when all tests pass", "her adım `pytest -k apps` ile doğrulanır") and
|
|
5510
|
+
# review round 2 of plan 142 caught both refused. Above `_admitted`:
|
|
5511
|
+
# "doğrulanmadı" repairs an unverified claim, and here nothing was checked.
|
|
5512
|
+
if read_only and not any(passing_check(r) for r in rows):
|
|
5513
|
+
ran = _asserted(RAN_IT, t)
|
|
5514
|
+
if ran:
|
|
5515
|
+
return ("plan claim",
|
|
5516
|
+
PLAN_CLAIM % cut(" ".join(ran.group(0).split()), 64))
|
|
4892
5517
|
fold = _admitted(fold, t)
|
|
4893
5518
|
# The trigger is the turn's own evidence, not its words. The claim vocabulary
|
|
4894
5519
|
# below catches a claim-shaped reply; it missed the same unfounded state
|
|
@@ -4915,7 +5540,12 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4915
5540
|
# `tail` or a read-only query owes no check. Its claim words are read the
|
|
4916
5541
|
# same way, and a claim is judged against the session's earlier turns below.
|
|
4917
5542
|
quiet = _bookkeeping_turn(rows, GIT_READS)
|
|
4918
|
-
|
|
5543
|
+
# A turn of reads that loop over files or read a read's output (plan 126:
|
|
5544
|
+
# `for f in $(find src); do cat -n "$f"; done`, refused in 4 of 6 live
|
|
5545
|
+
# advice turns) ends like one with no work - but only on a reply that
|
|
5546
|
+
# claims nothing. A done/tested claim over it keeps the worked turn's fold.
|
|
5547
|
+
read = quiet or _bookkeeping_turn(rows, GIT_READS, loops=True)
|
|
5548
|
+
if not worked or read:
|
|
4919
5549
|
# With no work, the words are the whole trigger, so a question or a
|
|
4920
5550
|
# negation is not read as the claim it names (`asserted_claims`).
|
|
4921
5551
|
done, verified = asserted_claims(t)
|
|
@@ -4939,12 +5569,13 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4939
5569
|
# exit below - "nothing was done and nothing was claimed" - is exactly the
|
|
4940
5570
|
# shape that used to leave such a claim unjudged.
|
|
4941
5571
|
external = _external_claim(t)
|
|
4942
|
-
if (not worked or
|
|
4943
|
-
return _residual_block(t, rows, shape, session_id=session_id, cwd=cwd
|
|
5572
|
+
if (not worked or read) and not (done or verified) and not external:
|
|
5573
|
+
return _residual_block(t, rows, shape, session_id=session_id, cwd=cwd,
|
|
5574
|
+
read_only=read_only)
|
|
4944
5575
|
if lost:
|
|
4945
5576
|
return _residual_block(t, rows, shape,
|
|
4946
5577
|
fold(rows, worked | {BEGAN_KIND}, external),
|
|
4947
|
-
session_id, cwd)
|
|
5578
|
+
session_id, cwd, read_only)
|
|
4948
5579
|
# A check that answered with an outcome nobody saw (exit 0 and nothing
|
|
4949
5580
|
# returned, a status a pipe owns: not `passing_check`) is no pass, and as a
|
|
4950
5581
|
# check it is no work row either. A done or tested claim resting on it owes
|
|
@@ -4977,7 +5608,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4977
5608
|
if not external and not unseen and _settled(srows):
|
|
4978
5609
|
return _residual_block(t, rows, shape, fold(
|
|
4979
5610
|
_before_turn(srows), True, None, "this session"),
|
|
4980
|
-
session_id, cwd)
|
|
5611
|
+
session_id, cwd, read_only)
|
|
4981
5612
|
if (not worked or quiet) and (done or verified):
|
|
4982
5613
|
# A claim in a turn that did nothing: "Tamamlandı, tüm testler
|
|
4983
5614
|
# geçti" after a turn that edited and ended "doğrulanmadı" was
|
|
@@ -4998,7 +5629,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4998
5629
|
if unseen:
|
|
4999
5630
|
worked = worked | {"verify_ok"}
|
|
5000
5631
|
return _residual_block(t, rows, shape, fold(judged, worked, external),
|
|
5001
|
-
session_id, cwd)
|
|
5632
|
+
session_id, cwd, read_only)
|
|
5002
5633
|
|
|
5003
5634
|
|
|
5004
5635
|
def _before_turn(rows):
|
|
@@ -5070,12 +5701,12 @@ def _evidence_text(cls, rows, external, where="this turn"):
|
|
|
5070
5701
|
% (where, shown or "a UI file"))
|
|
5071
5702
|
return ("no ui_ok",
|
|
5072
5703
|
"Design contract: %s changed a component (%s) and no "
|
|
5073
|
-
"
|
|
5704
|
+
"`~/.config/tezgah/bin/tezgah-design check` ran over a measurement, so nothing "
|
|
5074
5705
|
"says the component is on the repository's floor - a read of "
|
|
5075
5706
|
"the screen says what it looks like, not whether it is on "
|
|
5076
|
-
"that floor. Run
|
|
5707
|
+
"that floor. Run `~/.config/tezgah/bin/tezgah-design check --contract "
|
|
5077
5708
|
".tezgah/design-contract.md --measured <measurement.json>` "
|
|
5078
|
-
"(
|
|
5709
|
+
"(`~/.config/tezgah/bin/tezgah-design derive` writes the contract when the repo "
|
|
5079
5710
|
"has none; the measurement is the per-component styles and "
|
|
5080
5711
|
"states the analyze-app loop produces) and report every "
|
|
5081
5712
|
"violation it prints; if the check cannot run here, write "
|
|
@@ -5487,9 +6118,9 @@ RESIDUAL_WORK = (
|
|
|
5487
6118
|
# twice more.
|
|
5488
6119
|
STOPPED_SLICE = (
|
|
5489
6120
|
"{SLICE_LEAD}Slice %s, spawned by this session, stopped (%s) and "
|
|
5490
|
-
"its work is not resumed. Resume it with
|
|
6121
|
+
"its work is not resumed. Resume it with `~/.config/tezgah/bin/tezgah-task slice resume %s` - "
|
|
5491
6122
|
"the native path first, else the re-dispatch brief it prints - or, if the "
|
|
5492
|
-
"work was finished another way, record that with
|
|
6123
|
+
"work was finished another way, record that with `~/.config/tezgah/bin/tezgah-task slice done "
|
|
5493
6124
|
"%s`.")
|
|
5494
6125
|
|
|
5495
6126
|
# The `ready steps` class (plan 086, S1): the newest todo list (`note_todo`)
|
|
@@ -5514,6 +6145,27 @@ OPEN_TODO = (
|
|
|
5514
6145
|
"{ACCESS}, {IRREVERSIBLE} or {ENV}, or marked blocked in the todo list "
|
|
5515
6146
|
"with its reason.")
|
|
5516
6147
|
|
|
6148
|
+
# The `handed step` class (plan 142): the reply tells the reader to run a
|
|
6149
|
+
# command this session ran commands beside (`handed_steps`). Filled with up to
|
|
6150
|
+
# three of those lines.
|
|
6151
|
+
HANDED_STEP = (
|
|
6152
|
+
"{HANDED_LEAD}The reply hands the user %d step(s) this session can run "
|
|
6153
|
+
"itself: %s. Run each one now and report what it printed. A step stays the "
|
|
6154
|
+
"user's only when it is outward with no grant from this session, when it "
|
|
6155
|
+
"cannot be undone, or when it needs {ACCESS} or {ENV} - and then its own "
|
|
6156
|
+
"line says which, with the command that failed. In a host plan mode, where "
|
|
6157
|
+
"nothing can run, the plan is the deliverable and the step runs in the turn "
|
|
6158
|
+
"after the approval.")
|
|
6159
|
+
|
|
6160
|
+
# The `plan claim` class (plan 142): a done/tested claim made in a turn the
|
|
6161
|
+
# host ran in a read-only mode (`read_only_turn`), where no command and no
|
|
6162
|
+
# write was possible, so nothing can be behind it.
|
|
6163
|
+
PLAN_CLAIM = (
|
|
6164
|
+
"{PLAN_LEAD}This turn ran in the host's read-only plan mode: no command "
|
|
6165
|
+
"and no write was possible, so \"%s\" has nothing behind it. Say what the "
|
|
6166
|
+
"plan is and which command will prove it, and claim the result in the turn "
|
|
6167
|
+
"that runs it, after the plan is approved.")
|
|
6168
|
+
|
|
5517
6169
|
|
|
5518
6170
|
def _tr_lower(text):
|
|
5519
6171
|
"""`text` lowercased the Turkish way (I -> ı, İ -> i): `str.lower` reads
|
|
@@ -5831,6 +6483,10 @@ NEVER_DEFAULT = re.compile(alts("ask", "never_default"), re.I)
|
|
|
5831
6483
|
# A reply that names a plan it wrote, or asks to build one: building a plan the
|
|
5832
6484
|
# turn wrote is class F (plan 105), so asking it is the policy, not a needless ask
|
|
5833
6485
|
PLAN_BUILD = re.compile(alts("ask", "plan_build"), re.I)
|
|
6486
|
+
# A last line asking the owner to pick a variant or option a research, analysis
|
|
6487
|
+
# or review recommends, to take a way around a tezgah refusal, or to decide
|
|
6488
|
+
# about another session's work (its line, plan or branch): class F (plan 136)
|
|
6489
|
+
OWNER_CHOICE = re.compile(alts("ask", "owner_choice"), re.I)
|
|
5834
6490
|
SPEND = re.compile(alts("ask", "spend"), re.I)
|
|
5835
6491
|
AMOUNT = re.compile(r"\$\s?(\d+(?:\.\d+)?)|(\d+(?:\.\d+)?)\s?(?:\$|usd\b|dolar)", re.I)
|
|
5836
6492
|
DEFAULT_MARK = re.compile(alts("ask", "default_mark"), re.I)
|
|
@@ -5851,10 +6507,12 @@ NEEDLESS_ASK = (
|
|
|
5851
6507
|
"merge, a run parameter within the standing spend cap - are taken and "
|
|
5852
6508
|
"stated in one line, and an outward step the user granted this session is "
|
|
5853
6509
|
"taken, not asked again. An outward step the user has not granted waits "
|
|
5854
|
-
"for the answer and never proceeds on a default, and so
|
|
5855
|
-
"plan this turn wrote
|
|
5856
|
-
"
|
|
5857
|
-
"
|
|
6510
|
+
"for the answer and never proceeds on a default, and so do building a "
|
|
6511
|
+
"plan this turn wrote, choosing a variant or option a research, analysis "
|
|
6512
|
+
"or review recommends, a way around a tezgah refusal and deciding about "
|
|
6513
|
+
"another session's work. Do it now, or, when it is a taste or intent fork "
|
|
6514
|
+
"only the user can decide, put it under a \"{APPROVAL_HEADING}\" heading "
|
|
6515
|
+
"with a \"{RECOMMENDED}:\" default and proceed on that default.")
|
|
5858
6516
|
|
|
5859
6517
|
|
|
5860
6518
|
def outward_classes(text):
|
|
@@ -5902,7 +6560,9 @@ def needless_ask(text, granted=frozenset):
|
|
|
5902
6560
|
"""The question the reply ends on when the agent can decide it, else None.
|
|
5903
6561
|
|
|
5904
6562
|
The last line asks (QUESTION_END), its paragraph carries no recommended
|
|
5905
|
-
default (DEFAULT_MARK) and it names no class-F step (`never_default
|
|
6563
|
+
default (DEFAULT_MARK) and it names no class-F step (`never_default`, or
|
|
6564
|
+
OWNER_CHOICE: a variant or option to pick, a way around a refusal, another
|
|
6565
|
+
session's work); it
|
|
5906
6566
|
is then needless when it names an outward step (OUTWARD) the user granted
|
|
5907
6567
|
this session, every one it names (`granted()`, called only then, so a reply
|
|
5908
6568
|
that ends on no question reads no ledger), or, naming none, when it asks a
|
|
@@ -5913,7 +6573,8 @@ def needless_ask(text, granted=frozenset):
|
|
|
5913
6573
|
if not lines or not QUESTION_END.search(lines[-1]):
|
|
5914
6574
|
return None
|
|
5915
6575
|
last = lines[-1]
|
|
5916
|
-
if (never_default(last) or
|
|
6576
|
+
if (never_default(last) or OWNER_CHOICE.search(_tr_lower(last))
|
|
6577
|
+
or PLAN_BUILD.search(_tr_lower(t))
|
|
5917
6578
|
or DEFAULT_MARK.search(_tr_lower(re.split(r"\n\s*\n", t)[-1]))):
|
|
5918
6579
|
return None
|
|
5919
6580
|
low = _tr_lower(last)
|
|
@@ -5925,6 +6586,56 @@ def needless_ask(text, granted=frozenset):
|
|
|
5925
6586
|
return None
|
|
5926
6587
|
|
|
5927
6588
|
|
|
6589
|
+
# The "you run it" shape: a line that tells the reader to run something
|
|
6590
|
+
# (`ask.hand_over`), beside a command in backticks (CODE_SPAN, the module's
|
|
6591
|
+
# own: a fence line carries no backtick span and never qualifies).
|
|
6592
|
+
HAND_OVER = re.compile(alts("ask", "hand_over"), re.I)
|
|
6593
|
+
# The steps the owner runs by design, whatever the session can do: tezgah's own
|
|
6594
|
+
# consent command, its installation and a machine-level `brew` step (the
|
|
6595
|
+
# session-scope rule). An outward or class-F step is read off the line itself
|
|
6596
|
+
# (`outward_classes`, `never_default`).
|
|
6597
|
+
OWNER_STEP = re.compile(r"(?i)\btezgah-(?:consent|setup|update)\b|\bconsent\b|\bbrew\b")
|
|
6598
|
+
|
|
6599
|
+
|
|
6600
|
+
def read_only_turn(mode):
|
|
6601
|
+
"""True when the host says this turn ran in a mode that can neither write
|
|
6602
|
+
nor run (READ_ONLY_MODES). A host that sends nothing reads as False."""
|
|
6603
|
+
return str(mode or "").strip().lower().replace("_", "-") in READ_ONLY_MODES
|
|
6604
|
+
|
|
6605
|
+
|
|
6606
|
+
def handed_steps(text, rows, read_only=False):
|
|
6607
|
+
"""The lines that hand the reader a step this session could have taken.
|
|
6608
|
+
|
|
6609
|
+
A line qualifies when it names a command in backticks and tells the reader
|
|
6610
|
+
to run it (HAND_OVER: the reader addressed as the one who runs it, never
|
|
6611
|
+
the "you can run X" of an answer about how something is done). A table row
|
|
6612
|
+
(TABLE_ROW) and a quoted line are not
|
|
6613
|
+
the reply speaking: a refusal or a log the reply quotes carries the same
|
|
6614
|
+
words. It is kept - the ask policy's, not the agent's - when it names an
|
|
6615
|
+
outward step (`outward_classes`), a class-F step
|
|
6616
|
+
(`never_default`), the owner's consent or tezgah's own install
|
|
6617
|
+
(OWNER_STEP), or when the line types its blocker. The turn must have run a
|
|
6618
|
+
command of its own (COMMAND_KINDS): a turn that ran none could not have run
|
|
6619
|
+
this one either, which keeps the class inert on a host that sends no mode.
|
|
6620
|
+
`read_only` closes it outright: in a plan mode every step waits for the
|
|
6621
|
+
approval, which is what the contract tells the turn to do."""
|
|
6622
|
+
if read_only or not any(str(r.get("kind")) in COMMAND_KINDS for r in rows):
|
|
6623
|
+
return []
|
|
6624
|
+
out = []
|
|
6625
|
+
for line in str(text or "").splitlines():
|
|
6626
|
+
said = line.strip()
|
|
6627
|
+
if said.startswith(("|", ">")) or TABLE_ROW.match(line):
|
|
6628
|
+
continue
|
|
6629
|
+
if not (CODE_SPAN.search(said) and HAND_OVER.search(said)):
|
|
6630
|
+
continue
|
|
6631
|
+
prose = _prose(said)
|
|
6632
|
+
if (outward_classes(said) or never_default(said) or OWNER_STEP.search(said)
|
|
6633
|
+
or BLOCKER_SAID.search(prose) or BLOCKER_TRIED.search(prose)):
|
|
6634
|
+
continue
|
|
6635
|
+
out.append(said)
|
|
6636
|
+
return out
|
|
6637
|
+
|
|
6638
|
+
|
|
5928
6639
|
def undefaulted(inp):
|
|
5929
6640
|
"""(questions, with a default) of an ask-tool payload: omp's `ask` marks
|
|
5930
6641
|
the default with a question's `recommended` index, Claude's
|
|
@@ -5980,14 +6691,18 @@ def unblocked_items(text, rows):
|
|
|
5980
6691
|
|
|
5981
6692
|
|
|
5982
6693
|
def _residual_block(text, rows, shape, verdict=(None, None), session_id=None,
|
|
5983
|
-
cwd=None):
|
|
6694
|
+
cwd=None, read_only=False):
|
|
5984
6695
|
"""`verdict` when it refuses; else, in a turn that did work (a STEP_KINDS
|
|
5985
6696
|
row), the `residual work` class when `unblocked_items` names one, or when a
|
|
5986
6697
|
subagent ended in the turn handing up items no type accounts for (the
|
|
5987
6698
|
`residual` count on its `subagent_end` row) and this reply types none; else
|
|
5988
6699
|
the `needless ask` class when `needless_ask` names the question the reply
|
|
5989
6700
|
ends on (any turn, worked or not: the plan proposal that ends "Başlayayım
|
|
5990
|
-
mı?" is its measured case)
|
|
6701
|
+
mı?" is its measured case) - except in a read-only turn, where the host's
|
|
6702
|
+
plan mode asks for that approval and the agent cannot take the step it
|
|
6703
|
+
would be asked about; else the `handed step` class when `handed_steps`
|
|
6704
|
+
names a step the reply left to the reader; else the `ready steps` class
|
|
6705
|
+
when `ready_left`
|
|
5991
6706
|
names one; else the `open todo` class when `open_left` names one; else the
|
|
5992
6707
|
`stopped slice` class when `_stopped_slice` finds one; else `verdict`. Not
|
|
5993
6708
|
asked for a subagent's end (`shape` false) or a nested session: both
|
|
@@ -6019,9 +6734,15 @@ def _residual_block(text, rows, shape, verdict=(None, None), session_id=None,
|
|
|
6019
6734
|
move = next(filter(None, (_limit_move(item, failed, spawn_failed)
|
|
6020
6735
|
for _label, item in left)), None)
|
|
6021
6736
|
return ("residual work", RESIDUAL_WORK % named + (LIMIT_MOVE % move if move else ""))
|
|
6022
|
-
asked =
|
|
6737
|
+
asked = None if read_only else needless_ask(
|
|
6738
|
+
text, lambda: grants_in_force(session_id))
|
|
6023
6739
|
if asked:
|
|
6024
6740
|
return ("needless ask", NEEDLESS_ASK % cut(asked, 80))
|
|
6741
|
+
passed = handed_steps(text, rows, read_only)
|
|
6742
|
+
if passed:
|
|
6743
|
+
return ("handed step", HANDED_STEP % (
|
|
6744
|
+
len(passed), "; ".join(cut(" ".join(line.split()), 64)
|
|
6745
|
+
for line in passed[:3])))
|
|
6025
6746
|
todo = _turn_todo(rows, session_id)
|
|
6026
6747
|
ready = _ready_in(text, todo, session_id, cwd)
|
|
6027
6748
|
if ready:
|