@r1z4x/tezgah 1.9.2 → 1.11.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 +83 -1
- package/MANIFEST +382 -144
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.md +1 -1
- 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 +216 -40
- package/bin/tezgah-review-quote +185 -0
- package/bin/tezgah-setup +136 -33
- package/bin/tezgah-skill +1 -1
- package/bin/tezgah-status +201 -3
- package/bin/tezgah-task +203 -3
- package/commands/adhd.md +2 -2
- package/commands/ponytail.md +4 -4
- package/docs/architecture.md +2 -2
- package/docs/citations-baseline.json +2 -2
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +87 -42
- package/docs/environment.md +2 -0
- package/docs/evidence.md +28 -9
- package/docs/feature-audit.md +2 -2
- package/docs/gate.md +54 -31
- package/docs/glossary.md +5 -2
- package/docs/hosts.md +17 -3
- package/docs/index.json +34 -8
- package/docs/judge.md +4 -2
- package/docs/layers.md +3 -3
- package/docs/models.md +31 -9
- package/docs/operations.md +126 -1
- package/docs/research.md +786 -5
- package/docs/skills.md +137 -13
- package/docs/status-line.md +115 -1
- package/hooks/hooks.json +1 -1
- package/hooks/lang/en.json +10 -0
- package/hooks/lang/tr.json +11 -1
- package/hooks/tezgah_agents.py +38 -22
- package/hooks/tezgah_apps.py +18 -0
- package/hooks/tezgah_components.py +4 -0
- package/hooks/tezgah_context.py +189 -50
- package/hooks/tezgah_extension.py +337 -0
- package/hooks/tezgah_gate.py +188 -56
- package/hooks/tezgah_integrity.py +652 -105
- 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 +102 -6
- package/hooks/tezgah_policy.py +94 -45
- package/hooks/tezgah_rank.py +9 -6
- package/hooks/tezgah_replay.py +4 -4
- package/hooks/tezgah_research.py +1260 -65
- 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_stopspec.py +8 -1
- package/hooks/tezgah_task.py +12 -4
- package/hooks/tezgah_taste_ledger.py +1 -1
- package/hosts/cursor/hook.py +29 -2
- package/hosts/opencode/plugins/tezgah.js +307 -46
- package/hosts/opencode/tui/cli/tui.tsx +100 -0
- package/hosts/opencode/tui/tezgah-tui.tsx +2 -1
- package/output-styles/tezgah.md +10 -10
- 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 +15 -8
- package/skills/i-have-adhd/SKILL.md +22 -11
- package/skills/no-ai-slop/SKILL.md +15 -1
- package/skills/plan-add/SKILL.md +33 -26
- 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 +75 -39
- package/skills/rl-env/SKILL.md +25 -13
- package/skills/rl-env/SOURCE +8 -6
- package/skills/tezgah-contract/SKILL.md +47 -42
- 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",
|
|
@@ -1125,16 +1140,49 @@ def prior_calls(session_id, digest, tail=200, agent=None):
|
|
|
1125
1140
|
turn[-1].get("fail_class"))
|
|
1126
1141
|
|
|
1127
1142
|
|
|
1143
|
+
# Any `scheme://`: an internal channel (`agent://`, `local://`), a remote URL.
|
|
1144
|
+
# `file://` is read by `file_url_path`, the rest name no file on this disk.
|
|
1145
|
+
URI_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://")
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def file_url_path(text):
|
|
1149
|
+
"""The local path a `file://` URL names, `text` itself when it carries no
|
|
1150
|
+
URI scheme, or None for any other URI and for a file URL that names no local
|
|
1151
|
+
file (a host other than `localhost`, no path, a malformed host). Read as
|
|
1152
|
+
`<cwd>/file:/x`, a URL into another checkout landed in the cwd's, one file
|
|
1153
|
+
spelled both ways was two, and `agent://<id>` was a product file (plan 116).
|
|
1154
|
+
ponytail: a shell redirect `> file:///x` writes `./file:/x`; it is read as
|
|
1155
|
+
`/x` here, a spelling no tool produces."""
|
|
1156
|
+
if not URI_SCHEME.match(text):
|
|
1157
|
+
return text
|
|
1158
|
+
if text[:7].lower() != "file://":
|
|
1159
|
+
return None
|
|
1160
|
+
try:
|
|
1161
|
+
parts = urllib.parse.urlsplit(text)
|
|
1162
|
+
except ValueError: # an unclosed `[` host
|
|
1163
|
+
return None
|
|
1164
|
+
if parts.netloc.lower() not in ("", "localhost") or not parts.path:
|
|
1165
|
+
return None
|
|
1166
|
+
return urllib.parse.unquote(parts.path)
|
|
1167
|
+
|
|
1168
|
+
|
|
1128
1169
|
def _abs_target(path, cwd):
|
|
1129
|
-
"""`path` as an absolute real path, resolved against `cwd` when relative
|
|
1170
|
+
"""`path` as an absolute real path, resolved against `cwd` when relative, a
|
|
1171
|
+
local `file://` URL as the file it names; a URI that names no local file
|
|
1172
|
+
(`s3://b/k`, `agent://<id>`, a remote file URL) as its own spelling.
|
|
1130
1173
|
|
|
1131
1174
|
The form both halves of the cross-session write guard compare: the writer
|
|
1132
1175
|
stores it in an `edit` row's `target`, the gate resolves its own call's path
|
|
1133
1176
|
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
|
-
|
|
1177
|
+
the missing tail as it is), so a new file two sessions both create matches,
|
|
1178
|
+
and two sessions writing one `s3://` key match on its spelling, whatever
|
|
1179
|
+
their cwds. The no-plan rule skips such a target (`file_url_path` is None)."""
|
|
1180
|
+
raw = str(path or "").strip()
|
|
1181
|
+
if not raw:
|
|
1137
1182
|
return None
|
|
1183
|
+
path = file_url_path(raw)
|
|
1184
|
+
if path is None:
|
|
1185
|
+
return raw
|
|
1138
1186
|
if not os.path.isabs(path):
|
|
1139
1187
|
path = os.path.join(cwd or os.getcwd(), path)
|
|
1140
1188
|
return os.path.realpath(path)
|
|
@@ -1459,6 +1507,23 @@ def _week(ts):
|
|
|
1459
1507
|
return (int(ts or 0) - MONDAY) // WEEK * WEEK + MONDAY
|
|
1460
1508
|
|
|
1461
1509
|
|
|
1510
|
+
def day_start(text):
|
|
1511
|
+
"""The epoch second a `YYYY-MM-DD` date starts at, in this machine's time
|
|
1512
|
+
zone, or None when the text is not that date. Whole calendar days are what
|
|
1513
|
+
a reader types and what a person reads on a calendar, so the window edges
|
|
1514
|
+
are local midnights; one format, never a guess at a date that was mistyped."""
|
|
1515
|
+
try:
|
|
1516
|
+
return int(time.mktime(time.strptime(str(text), "%Y-%m-%d")))
|
|
1517
|
+
except (TypeError, ValueError, OverflowError):
|
|
1518
|
+
return None
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _day(ts):
|
|
1522
|
+
"""The calendar day a row's `ts` falls on, as `YYYY-MM-DD` (local, the same
|
|
1523
|
+
edges `day_start` reads)."""
|
|
1524
|
+
return time.strftime("%Y-%m-%d", time.localtime(int(ts)))
|
|
1525
|
+
|
|
1526
|
+
|
|
1462
1527
|
_CATALOG = {}
|
|
1463
1528
|
|
|
1464
1529
|
|
|
@@ -1727,6 +1792,18 @@ def _reissued(rows):
|
|
|
1727
1792
|
return out
|
|
1728
1793
|
|
|
1729
1794
|
|
|
1795
|
+
def _claim_class(detail):
|
|
1796
|
+
"""How a `claim` row's detail classifies: `(class, verdict)` where verdict is
|
|
1797
|
+
`ok` (nothing was refused), `shape` (the class in `SHAPE_BLOCKS` refuses the
|
|
1798
|
+
reply's form, not its claim) or `blocked` (the class refuses the claim).
|
|
1799
|
+
One reader for `_counts` and `rates`, so a corpus total and a window cannot
|
|
1800
|
+
disagree about what a `list cap` row is."""
|
|
1801
|
+
if not str(detail).startswith("blocked"):
|
|
1802
|
+
return None, "ok"
|
|
1803
|
+
cls = str(detail)[len("blocked: "):]
|
|
1804
|
+
return cls, ("shape" if cls in SHAPE_BLOCKS else "blocked")
|
|
1805
|
+
|
|
1806
|
+
|
|
1730
1807
|
def _counts(rows, weeks=False, tools=False):
|
|
1731
1808
|
"""`counters`' arithmetic over rows already read: the one implementation
|
|
1732
1809
|
both readers fold with, so a total and the sessions it sums cannot drift
|
|
@@ -1807,19 +1884,18 @@ def _counts(rows, weeks=False, tools=False):
|
|
|
1807
1884
|
elif kind == "claim":
|
|
1808
1885
|
out["claims"] += 1
|
|
1809
1886
|
refused = False
|
|
1810
|
-
|
|
1887
|
+
cls, verdict = _claim_class(detail)
|
|
1888
|
+
if verdict == "shape":
|
|
1811
1889
|
# a reply blocked for its shape made no false claim: the rate
|
|
1812
1890
|
# 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
|
|
1891
|
+
out["shape_blocked"] += 1
|
|
1892
|
+
elif verdict == "blocked":
|
|
1893
|
+
# split by the Stop class: which check refused, as a number
|
|
1894
|
+
out["blocked_claims"][cls] = \
|
|
1895
|
+
out["blocked_claims"].get(cls, 0) + 1
|
|
1896
|
+
if cls not in LIST_BLOCKS:
|
|
1897
|
+
out["false_completion"] += 1
|
|
1898
|
+
refused = True
|
|
1823
1899
|
if weeks:
|
|
1824
1900
|
# the same bucket this row's events went into, so a claim cannot
|
|
1825
1901
|
# sit in a week's total and outside its ratio
|
|
@@ -1884,27 +1960,204 @@ def _counts(rows, weeks=False, tools=False):
|
|
|
1884
1960
|
return out
|
|
1885
1961
|
|
|
1886
1962
|
|
|
1887
|
-
|
|
1963
|
+
# The windowed rates reader (plan 134). One bucket is filled once for the window
|
|
1964
|
+
# and once for each of its calendar days from the same rows, so a day and the
|
|
1965
|
+
# window it sits in cannot disagree, and two adjacent windows partition the span
|
|
1966
|
+
# because the filter is `[start, end)` on the row's own `ts`.
|
|
1967
|
+
def _rate_bucket():
|
|
1968
|
+
"""An empty rates bucket: the counts the observation rows read, and the two
|
|
1969
|
+
histograms they split them by."""
|
|
1970
|
+
return {"events": 0, "turns": 0, "claims": 0, "replies": 0,
|
|
1971
|
+
"blocked_claims": {}, "shape_blocked": 0, "false_completion": 0,
|
|
1972
|
+
"shape_flagged": 0, "shapes": {}, "denies": {}, "row_versions": {}}
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
# A row's contract version as the rates name it, the label `failure_shapes`
|
|
1976
|
+
# already prints: `v1`, or `unversioned` for a row the writer stamped nothing on.
|
|
1977
|
+
def _contract(value):
|
|
1978
|
+
return "unversioned" if value is None else "v%s" % value
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
def _rate_row(bucket, entry):
|
|
1982
|
+
"""One ledger row into a rates bucket: the classification `_counts` makes,
|
|
1983
|
+
and only the rows these rates read."""
|
|
1984
|
+
kind = str(entry.get("kind") or "")
|
|
1985
|
+
detail = str(entry.get("detail") or "")
|
|
1986
|
+
bucket["events"] += 1
|
|
1987
|
+
bucket["row_versions"][_contract(entry.get("v"))] = \
|
|
1988
|
+
bucket["row_versions"].get(_contract(entry.get("v")), 0) + 1
|
|
1989
|
+
if kind == TURN_KIND:
|
|
1990
|
+
bucket["turns"] += 1
|
|
1991
|
+
elif kind == "deny":
|
|
1992
|
+
rule = _deny_rule(detail)
|
|
1993
|
+
bucket["denies"][rule] = bucket["denies"].get(rule, 0) + 1
|
|
1994
|
+
elif kind == "claim":
|
|
1995
|
+
bucket["claims"] += 1
|
|
1996
|
+
cls, verdict = _claim_class(detail)
|
|
1997
|
+
if verdict == "shape":
|
|
1998
|
+
bucket["shape_blocked"] += 1
|
|
1999
|
+
elif verdict == "blocked":
|
|
2000
|
+
bucket["blocked_claims"][cls] = \
|
|
2001
|
+
bucket["blocked_claims"].get(cls, 0) + 1
|
|
2002
|
+
if cls not in LIST_BLOCKS:
|
|
2003
|
+
bucket["false_completion"] += 1
|
|
2004
|
+
elif kind == "shape":
|
|
2005
|
+
# one row per judged reply, its detail the flag list (`ok` when none):
|
|
2006
|
+
# `shape_flagged` is the reply count the rate needs, `shapes` the class
|
|
2007
|
+
# split - a count of flags, so a reply carrying two moves both
|
|
2008
|
+
bucket["replies"] += 1
|
|
2009
|
+
if detail and detail != "ok":
|
|
2010
|
+
bucket["shape_flagged"] += 1
|
|
2011
|
+
for flag in (f.strip() for f in detail.split(",")):
|
|
2012
|
+
if flag:
|
|
2013
|
+
bucket["shapes"][flag] = bucket["shapes"].get(flag, 0) + 1
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
def _per_scale(scale, numerator, denominator):
|
|
2017
|
+
"""`scale * n / d`, or None with no denominator: a window with no turn or no
|
|
2018
|
+
session has no rate, and a 0.0 there would read as a measured zero."""
|
|
2019
|
+
return round(scale * numerator / denominator, 4) if denominator else None
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
def _window_rates(bucket, sessions):
|
|
2023
|
+
"""The rates plan 066's rows read, off a filled bucket."""
|
|
2024
|
+
blocked = sum(bucket["blocked_claims"].values())
|
|
2025
|
+
return {
|
|
2026
|
+
# plan 134's pair: what a block and a deny cost per unit of use
|
|
2027
|
+
"blocks_per_100_turns": _per_scale(100, blocked, bucket["turns"]),
|
|
2028
|
+
"denies_per_100_sessions": _per_scale(
|
|
2029
|
+
100, sum(bucket["denies"].values()), sessions),
|
|
2030
|
+
# 056's bar compares these two across adjacent windows, in pp: the share
|
|
2031
|
+
# of claims a Stop class refused, and the share of judged replies that
|
|
2032
|
+
# carried a report-only shape flag
|
|
2033
|
+
"claim_block_rate": _per_scale(1, blocked, bucket["claims"]),
|
|
2034
|
+
"stop_shape_rate": _per_scale(1, bucket["shape_flagged"],
|
|
2035
|
+
bucket["replies"]),
|
|
2036
|
+
# 051's bar is per class against live sessions: 1 per 500 reads 0.2 here
|
|
2037
|
+
"blocked_claims_per_100_sessions": {
|
|
2038
|
+
cls: _per_scale(100, n, sessions)
|
|
2039
|
+
for cls, n in bucket["blocked_claims"].items()},
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
|
|
2043
|
+
def rates(since, until):
|
|
2044
|
+
"""Every real-session ledger's rows stamped in `[since, until)`, folded per
|
|
2045
|
+
window and per calendar day: turns, `claim` rows and the Stop classes that
|
|
2046
|
+
blocked them, the shape flags every judged reply carried, denies by rule,
|
|
2047
|
+
and the rates plan 066's observation rows read (plan 134).
|
|
2048
|
+
|
|
2049
|
+
The window is this reader's; `counters_all` still folds a lifetime, because
|
|
2050
|
+
a total that contradicted the sessions it sums would be the wrong answer to
|
|
2051
|
+
its own question. `until` is exclusive, so two adjacent windows are a
|
|
2052
|
+
partition of the span and not an approximation of one - no row is counted
|
|
2053
|
+
twice or dropped at the seam. A row with no `ts` (a `ledger_damage` row
|
|
2054
|
+
stands in for bytes that could not be parsed) falls outside every window: it
|
|
2055
|
+
carries no time to place it in. `sessions` counts the real ledgers with at
|
|
2056
|
+
least one row in the window, the denominator 051's "live sessions" names;
|
|
2057
|
+
`days` is the window's own length and `by_day` holds only the days that saw
|
|
2058
|
+
a row. Both dates are `YYYY-MM-DD` local calendar days; a malformed or
|
|
2059
|
+
reversed pair is ValueError, never a silently empty window."""
|
|
2060
|
+
start, end = day_start(since), day_start(until)
|
|
2061
|
+
if start is None or end is None:
|
|
2062
|
+
raise ValueError("rates wants --since and --until as YYYY-MM-DD")
|
|
2063
|
+
if end <= start:
|
|
2064
|
+
raise ValueError("rates wants --until after --since")
|
|
2065
|
+
total = _rate_bucket()
|
|
2066
|
+
days, sessions = {}, 0
|
|
2067
|
+
for path in ledgers():
|
|
2068
|
+
rows = _foreign_rows(path)
|
|
2069
|
+
if fixture_ledger(rows):
|
|
2070
|
+
continue
|
|
2071
|
+
rows = [r for r in rows if isinstance(r.get("ts"), (int, float))
|
|
2072
|
+
and start <= r["ts"] < end]
|
|
2073
|
+
if not rows:
|
|
2074
|
+
continue
|
|
2075
|
+
sessions += 1
|
|
2076
|
+
for entry in rows:
|
|
2077
|
+
_rate_row(total, entry)
|
|
2078
|
+
_rate_row(days.setdefault(_day(entry["ts"]), _rate_bucket()), entry)
|
|
2079
|
+
return dict(total, since=str(since), until=str(until), sessions=sessions,
|
|
2080
|
+
days=int(round((end - start) / 86400.0)),
|
|
2081
|
+
by_day=[dict(bucket, day=day)
|
|
2082
|
+
for day, bucket in sorted(days.items())],
|
|
2083
|
+
rates=_window_rates(total, sessions))
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
def _check_runs(cmd, cwd=None):
|
|
1888
2087
|
"""(name, mode) for every check-shaped command in `cmd`, in order. `mode` is
|
|
1889
2088
|
`check`, `info` (INFO_ARGS / INFO_SUBCOMMAND: it checks nothing) or `write`
|
|
1890
2089
|
(FORMAT_WRITE: it changes the tree), read on the words after the tool up to
|
|
1891
2090
|
the end of that one command, so `pytest --version; pytest` still runs a
|
|
1892
|
-
check.
|
|
2091
|
+
check. With `cwd`, the repository's own declared checks (`repo_checks`)
|
|
2092
|
+
follow the built-in vocabulary's."""
|
|
1893
2093
|
text = mask(cmd)
|
|
1894
2094
|
for m in VERIFY.finditer(text):
|
|
1895
2095
|
name = (m.groupdict().get("js") or m.group(0)).strip()
|
|
1896
2096
|
tool = name.split()[-1].lower()
|
|
1897
2097
|
args = COMMAND_END.split(text[m.end():], 1)[0]
|
|
1898
2098
|
sub = INFO_SUBCOMMAND.get(tool)
|
|
1899
|
-
|
|
2099
|
+
# the info flag may sit among the runner's own flags
|
|
2100
|
+
# (`npm --help test` runs nothing), so the whole match is read too
|
|
2101
|
+
if INFO_ARGS.search(m.group(0)) or INFO_ARGS.search(args) \
|
|
2102
|
+
or (sub and sub.match(args)):
|
|
1900
2103
|
yield name, "info"
|
|
1901
2104
|
elif tool in FORMAT_WRITE and FORMAT_WRITE[tool].search(args):
|
|
1902
2105
|
yield name, "write"
|
|
1903
2106
|
else:
|
|
1904
2107
|
yield name, "check"
|
|
2108
|
+
prefixes = repo_checks(cwd)
|
|
2109
|
+
if prefixes:
|
|
2110
|
+
yield from _repo_check_runs(cmd, prefixes)
|
|
2111
|
+
|
|
2112
|
+
|
|
2113
|
+
# The words in front of a program that leave it the program: VERIFY's own
|
|
2114
|
+
# wrapper prefix, read on words instead of text.
|
|
2115
|
+
CHECK_WRAPPERS = frozenset(("sudo", "env", "time", "nohup", "command", "exec"))
|
|
2116
|
+
ASSIGNMENT_WORD = re.compile(r"^[A-Za-z_]\w*=")
|
|
1905
2117
|
|
|
1906
2118
|
|
|
1907
|
-
def
|
|
2119
|
+
def repo_checks(cwd):
|
|
2120
|
+
"""[argv prefix] the repository at `cwd` declares as checks in its manifest
|
|
2121
|
+
(`tezgah_extension.repo_checks`), or [] - no cwd, no manifest, a broken one,
|
|
2122
|
+
or a missing extension module. Imported here, not at the top: a broken
|
|
2123
|
+
extension costs the repository its declared checks, never the hook."""
|
|
2124
|
+
if not cwd:
|
|
2125
|
+
return []
|
|
2126
|
+
try:
|
|
2127
|
+
import tezgah_extension
|
|
2128
|
+
return tezgah_extension.repo_checks(cwd)
|
|
2129
|
+
except Exception:
|
|
2130
|
+
return []
|
|
2131
|
+
|
|
2132
|
+
|
|
2133
|
+
def _repo_check_runs(cmd, prefixes):
|
|
2134
|
+
"""(name, mode) for each simple command of `cmd` that starts with one of
|
|
2135
|
+
`prefixes`, after assignments and VERIFY's wrappers. Read with
|
|
2136
|
+
`_shell_commands`, so a prefix inside a quoted argument, a commit message or
|
|
2137
|
+
a QUOTED heredoc body (`<<'EOF'`, `<<"EOF"`) is an argument, not a run - the
|
|
2138
|
+
masked reading VERIFY keeps. An unquoted heredoc's body (`<<EOF`, `<<-EOF`)
|
|
2139
|
+
is read as commands, so `cat > x <<EOF` over a `./bin/run-tool` line counts
|
|
2140
|
+
as a run, as a `pytest` line there does for VERIFY.
|
|
2141
|
+
ponytail: a wrapper's own option (`sudo -u root ./bin/x`) hides the program,
|
|
2142
|
+
a lost credit rather than an invented one."""
|
|
2143
|
+
for words, _sep in _shell_commands(cmd):
|
|
2144
|
+
i = 0
|
|
2145
|
+
while i < len(words) and (words[i] in CHECK_WRAPPERS
|
|
2146
|
+
or ASSIGNMENT_WORD.match(words[i])):
|
|
2147
|
+
i += 1
|
|
2148
|
+
run = words[i:]
|
|
2149
|
+
if not run:
|
|
2150
|
+
continue
|
|
2151
|
+
head = [os.path.normpath(run[0])] + run[1:]
|
|
2152
|
+
for prefix in prefixes:
|
|
2153
|
+
if head[:len(prefix)] == prefix:
|
|
2154
|
+
name = " ".join(run[:len(prefix)])
|
|
2155
|
+
rest = " ".join(run[len(prefix):])
|
|
2156
|
+
yield name, ("info" if INFO_ARGS.search(rest) else "check")
|
|
2157
|
+
break
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
def verify_command(cmd, cwd=None):
|
|
1908
2161
|
"""The name of the check this command runs, or None.
|
|
1909
2162
|
|
|
1910
2163
|
The scan runs on the masked text, the convention `shortcut_command` already
|
|
@@ -1920,8 +2173,9 @@ def verify_command(cmd):
|
|
|
1920
2173
|
(`_check_runs`), so every consumer - the evidence kind, the gate's retry
|
|
1921
2174
|
exemption, its began `check` mark, `piped_check` and the NEUTER rule - reads
|
|
1922
2175
|
them as the plain commands they are. A JS tool names itself, not its runner
|
|
1923
|
-
(`npx axe-core` is `axe-core`).
|
|
1924
|
-
|
|
2176
|
+
(`npx axe-core` is `axe-core`). `cwd` adds the repository's declared checks
|
|
2177
|
+
(`repo_checks`); without it only the built-in vocabulary counts."""
|
|
2178
|
+
return next((name for name, mode in _check_runs(cmd, cwd) if mode == "check"),
|
|
1925
2179
|
None)
|
|
1926
2180
|
|
|
1927
2181
|
|
|
@@ -1994,6 +2248,46 @@ def _closes(cmd, pos, tag, strip, message):
|
|
|
1994
2248
|
return None, n
|
|
1995
2249
|
|
|
1996
2250
|
|
|
2251
|
+
# bash's metacharacters: unquoted, each one ends a word. Any other character -
|
|
2252
|
+
# an escaped or quoted one, a `\r`, a non-breaking space - is part of a word.
|
|
2253
|
+
WORD_BREAKS = " \t\n;&|()<>"
|
|
2254
|
+
# A line continuation: outside single quotes and comments bash deletes both
|
|
2255
|
+
# characters, so the words around it join (`-de\<nl>lete` is `-delete`) and
|
|
2256
|
+
# the word before it goes on (`x\<nl>#` is the word `x#`). After an escaped
|
|
2257
|
+
# backslash (`a\\<nl>`) the newline is a real one and ends the command.
|
|
2258
|
+
CONTINUATION = "\\\n"
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
def _opens_comment(prev):
|
|
2262
|
+
"""True when an unquoted, unescaped `#` opens a comment: only at the start
|
|
2263
|
+
of a word. `prev` is the last character bash kept before it - "" at the
|
|
2264
|
+
start of the text, a non-break for an escaped or quoted one, and the one
|
|
2265
|
+
before a CONTINUATION, which is no character. The one rule every shell
|
|
2266
|
+
reader asks (`_heredocs`, `_shell_lines`, `mask`, `_shell_read`, and the
|
|
2267
|
+
gate's `_shell_words` through `_shell_lines`), so none of them reads a
|
|
2268
|
+
comment where bash runs a command (review of plan 126, round 3h)."""
|
|
2269
|
+
return not prev or prev in WORD_BREAKS
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
# What before a `(` makes it part of a word: a command or process substitution
|
|
2273
|
+
# or an arithmetic expansion (`$(`, `<(`, `>(`, `$((`) and an array (`x=(`).
|
|
2274
|
+
WORD_PARENS = "$<>="
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
def _kept(ch, before, parens):
|
|
2278
|
+
"""`ch` as the `prev` `_opens_comment` asks of the character after it: a
|
|
2279
|
+
`)` closing a `(` opened right after WORD_PARENS ends no word (`$(:)#x` is
|
|
2280
|
+
one word, so bash runs a command after it), and reads as "_"; a subshell's,
|
|
2281
|
+
an arithmetic command's or a `case` pattern's `)` is a break. `before` is
|
|
2282
|
+
the character kept before `ch` and `parens` the reader's stack of open `(`
|
|
2283
|
+
(review of plan 126, round 3i)."""
|
|
2284
|
+
if ch == "(":
|
|
2285
|
+
parens.append(bool(before) and before in WORD_PARENS)
|
|
2286
|
+
elif ch == ")" and parens and parens.pop():
|
|
2287
|
+
return "_"
|
|
2288
|
+
return ch
|
|
2289
|
+
|
|
2290
|
+
|
|
1997
2291
|
def _heredocs(cmd):
|
|
1998
2292
|
"""Every heredoc operator in `cmd`, in order, as (operator start, operator
|
|
1999
2293
|
end, tag, quoted, body start, terminator span or None, safe).
|
|
@@ -2015,10 +2309,13 @@ def _heredocs(cmd):
|
|
|
2015
2309
|
bash runs - so the reader no longer decides where such a context ends: it
|
|
2016
2310
|
shows the text. A false refusal of data is the accepted cost
|
|
2017
2311
|
(tests/bash_vectors.ACCEPTED_REFUSALS)."""
|
|
2018
|
-
n, found, pending, i = len(cmd), [], [], 0
|
|
2019
|
-
quote, depth, sure, line_start = None, 0, True, 0
|
|
2312
|
+
n, found, pending, i, parens = len(cmd), [], [], 0, []
|
|
2313
|
+
quote, depth, sure, line_start, last = None, 0, True, 0, ""
|
|
2020
2314
|
while i < n:
|
|
2021
2315
|
ch = cmd[i]
|
|
2316
|
+
# `last` is the character bash kept before `ch`: a quoted or escaped
|
|
2317
|
+
# one leaves "_", a non-break, unless a branch below says otherwise
|
|
2318
|
+
before, last = last, "_"
|
|
2022
2319
|
if quote in ("'", "$'"):
|
|
2023
2320
|
if ch == "\\" and quote == "$'":
|
|
2024
2321
|
i += 2
|
|
@@ -2028,6 +2325,8 @@ def _heredocs(cmd):
|
|
|
2028
2325
|
i += 1
|
|
2029
2326
|
continue
|
|
2030
2327
|
if ch == "\\":
|
|
2328
|
+
if cmd.startswith(CONTINUATION, i):
|
|
2329
|
+
last = before
|
|
2031
2330
|
i += 2
|
|
2032
2331
|
continue
|
|
2033
2332
|
if quote == '"':
|
|
@@ -2039,7 +2338,7 @@ def _heredocs(cmd):
|
|
|
2039
2338
|
continue
|
|
2040
2339
|
if ch == "\n":
|
|
2041
2340
|
if not pending:
|
|
2042
|
-
line_start = i + 1
|
|
2341
|
+
line_start, last = i + 1, ch
|
|
2043
2342
|
i += 1
|
|
2044
2343
|
continue
|
|
2045
2344
|
pos = i + 1
|
|
@@ -2053,7 +2352,7 @@ def _heredocs(cmd):
|
|
|
2053
2352
|
pos = i + 1
|
|
2054
2353
|
pending = []
|
|
2055
2354
|
continue
|
|
2056
|
-
if ch == "#" and (
|
|
2355
|
+
if ch == "#" and _opens_comment(before):
|
|
2057
2356
|
end = cmd.find("\n", i)
|
|
2058
2357
|
i = n if end < 0 else end
|
|
2059
2358
|
continue
|
|
@@ -2078,7 +2377,7 @@ def _heredocs(cmd):
|
|
|
2078
2377
|
i += 1
|
|
2079
2378
|
continue
|
|
2080
2379
|
if cmd.startswith("<<<", i):
|
|
2081
|
-
i
|
|
2380
|
+
i, last = i + 3, "<"
|
|
2082
2381
|
continue
|
|
2083
2382
|
if cmd.startswith("<<", i):
|
|
2084
2383
|
j = i + 2
|
|
@@ -2102,7 +2401,7 @@ def _heredocs(cmd):
|
|
|
2102
2401
|
depth += 1
|
|
2103
2402
|
elif ch in ")]}":
|
|
2104
2403
|
depth -= 1
|
|
2105
|
-
i
|
|
2404
|
+
i, last = i + 1, _kept(ch, before, parens)
|
|
2106
2405
|
found += [(s, e, t, q, n, None, False) for s, e, t, q, _, _ in pending]
|
|
2107
2406
|
return found
|
|
2108
2407
|
|
|
@@ -2151,50 +2450,81 @@ def _heredoc_bytes(text):
|
|
|
2151
2450
|
|
|
2152
2451
|
def mask(text):
|
|
2153
2452
|
"""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.
|
|
2453
|
+
length kept, read the way bash reads it: `'...'` takes no escape, `$'...'`
|
|
2454
|
+
(an unescaped `$` only) and `"..."` do, a `\\` outside quotes escapes one
|
|
2455
|
+
character, and `#` starts a comment only at the start of a word. So
|
|
2456
|
+
`https://x`, `a#b`, `src/*.py ... lib/*/`, `'x\\'` and `\\$'x\\'` are words,
|
|
2457
|
+
not a comment or an open string that blanks the command after them
|
|
2458
|
+
(gate-01). A quote left open blanks from itself to the end - bash runs
|
|
2459
|
+
nothing after it - and keeps the reading before it. A CONTINUATION bash
|
|
2460
|
+
deletes (outside single quotes, `$'...'` and comments) is moved to its
|
|
2461
|
+
word's start as two spaces, so every rule reads the token bash joins
|
|
2462
|
+
(`tr\\<nl>ue` is `true`, `|\\<nl>|` is `||`) at offsets `joined` keeps
|
|
2463
|
+
(review of plan 126, round 3i).
|
|
2161
2464
|
|
|
2162
2465
|
Memoised per process: a pure function of the text, and one Stop asks it of
|
|
2163
2466
|
the same ledger details from several readers (both Stop folds, plan 063)."""
|
|
2164
|
-
return _mask(str(text or ""))
|
|
2467
|
+
return _mask(str(text or ""))[0]
|
|
2468
|
+
|
|
2469
|
+
|
|
2470
|
+
def joined(text):
|
|
2471
|
+
"""The raw `text` with each CONTINUATION moved as `mask` moves it: the text
|
|
2472
|
+
a rule slices at the offsets a masked match proved."""
|
|
2473
|
+
text = str(text or "")
|
|
2474
|
+
return _moved(list(text), _mask(text)[1])
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
def _moved(out, cuts):
|
|
2478
|
+
for word, at in cuts:
|
|
2479
|
+
out[word + 2:at + 2] = out[word:at]
|
|
2480
|
+
out[word:word + 2] = " "
|
|
2481
|
+
return "".join(out)
|
|
2165
2482
|
|
|
2166
2483
|
|
|
2167
2484
|
@functools.lru_cache(maxsize=4096)
|
|
2168
2485
|
def _mask(text):
|
|
2486
|
+
"""(the masked text, the (word start, offset) of each CONTINUATION moved)."""
|
|
2169
2487
|
text = _blank_heredocs(text)
|
|
2170
2488
|
body = _heredoc_bytes(text)
|
|
2171
2489
|
out, i, n, start, prev = list(text), 0, len(text), True, ""
|
|
2490
|
+
parens, word, cuts = [], 0, []
|
|
2172
2491
|
while i < n:
|
|
2173
2492
|
ch = text[i]
|
|
2174
2493
|
if body[i]:
|
|
2175
|
-
i, start, prev = i + 1, True, ""
|
|
2494
|
+
i, start, prev, word = i + 1, True, "", i + 1
|
|
2176
2495
|
continue
|
|
2177
2496
|
if ch == "\\":
|
|
2178
|
-
|
|
2497
|
+
# a CONTINUATION is no character: the word state before it goes on
|
|
2498
|
+
if text.startswith(CONTINUATION, i):
|
|
2499
|
+
cuts.append((word, i))
|
|
2500
|
+
else:
|
|
2501
|
+
start, prev = False, ""
|
|
2502
|
+
i += 2
|
|
2179
2503
|
continue
|
|
2180
2504
|
if ch in "'\"":
|
|
2181
2505
|
escapes = ch == '"' or prev == "$"
|
|
2182
2506
|
end = i + 1
|
|
2183
2507
|
while end < n and text[end] != ch:
|
|
2184
|
-
|
|
2508
|
+
if escapes and text[end] == "\\":
|
|
2509
|
+
if ch == '"' and text.startswith(CONTINUATION, end):
|
|
2510
|
+
cuts.append((word, end))
|
|
2511
|
+
end += 1
|
|
2512
|
+
end += 1
|
|
2185
2513
|
end = min(end + 1, n)
|
|
2186
2514
|
elif ch == "#" and start:
|
|
2187
2515
|
end = text.find("\n", i)
|
|
2188
2516
|
end = n if end < 0 else end
|
|
2189
2517
|
else:
|
|
2190
|
-
start, prev =
|
|
2518
|
+
start, prev = _opens_comment(_kept(ch, prev, parens)), ch
|
|
2191
2519
|
i += 1
|
|
2520
|
+
if ch in " \t\n":
|
|
2521
|
+
word = i
|
|
2192
2522
|
continue
|
|
2193
2523
|
for k in range(i, end):
|
|
2194
2524
|
if out[k] != "\n":
|
|
2195
2525
|
out[k] = " "
|
|
2196
2526
|
i, start, prev = end, False, ""
|
|
2197
|
-
return
|
|
2527
|
+
return _moved(out, cuts), tuple(cuts)
|
|
2198
2528
|
|
|
2199
2529
|
|
|
2200
2530
|
def _unquoted_backticks(text):
|
|
@@ -2215,6 +2545,17 @@ def _unquoted_backticks(text):
|
|
|
2215
2545
|
elif ch in "'\"":
|
|
2216
2546
|
quote = ch
|
|
2217
2547
|
elif ch == "`":
|
|
2548
|
+
# a `#` right after it, continuations aside, opens no comment
|
|
2549
|
+
# (`_opens_comment` sees `;`): bash reads it mid-word after the
|
|
2550
|
+
# close, and a comment inside the body ends at the close, so it is
|
|
2551
|
+
# escaped here (review of plan 126, round 3i: `` `:`\<nl># ``)
|
|
2552
|
+
j = i + 1
|
|
2553
|
+
while text.startswith(CONTINUATION, j):
|
|
2554
|
+
j += 2
|
|
2555
|
+
if text.startswith("#", j):
|
|
2556
|
+
out.append(";" + text[i + 1:j] + "\\#")
|
|
2557
|
+
i = j + 1
|
|
2558
|
+
continue
|
|
2218
2559
|
ch = ";"
|
|
2219
2560
|
out.append(ch)
|
|
2220
2561
|
i += 1
|
|
@@ -2257,28 +2598,32 @@ def _for_shlex(text):
|
|
|
2257
2598
|
def _shell_lines(text):
|
|
2258
2599
|
"""`text` split at the newlines that end a command in bash: none inside
|
|
2259
2600
|
quotes (`'...'`, `"..."`, `$'...'`), each one in a heredoc body or its
|
|
2260
|
-
terminator (`_heredoc_bytes`),
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2601
|
+
terminator (`_heredoc_bytes`), a CONTINUATION deleted outside single quotes
|
|
2602
|
+
(in `$'...'` it stays, as bash keeps it), and a comment - `#` where
|
|
2603
|
+
`_opens_comment` - dropped to its line end. A double-quoted string holding
|
|
2604
|
+
a `$( )` or a backtick runs a command, so its newlines still split - the
|
|
2605
|
+
reading this had before, which keeps that command visible. A quote left
|
|
2606
|
+
open at the end goes back to a split at every newline from its line on."""
|
|
2265
2607
|
body = _heredoc_bytes(text)
|
|
2266
|
-
lines, cur, quote,
|
|
2267
|
-
opened, runs = 0, False
|
|
2608
|
+
lines, cur, quote, prev, i, n = [], [], None, "", 0, len(text)
|
|
2609
|
+
opened, runs, parens = 0, False, []
|
|
2268
2610
|
while i < n:
|
|
2269
2611
|
ch = text[i]
|
|
2270
2612
|
if body[i] or (not quote or runs) and ch in "\r\n":
|
|
2271
2613
|
if ch in "\r\n":
|
|
2272
2614
|
lines.append("".join(cur))
|
|
2273
|
-
cur, quote,
|
|
2274
|
-
|
|
2615
|
+
cur, quote, runs = [], None, False
|
|
2616
|
+
step = 2 if text[i:i + 2] == "\r\n" else 1
|
|
2617
|
+
# a lone `\r` is no word break to bash: the split shows more
|
|
2618
|
+
prev, i = text[i + step - 1], i + step
|
|
2275
2619
|
else:
|
|
2276
2620
|
cur.append(ch)
|
|
2277
2621
|
i += 1
|
|
2278
2622
|
continue
|
|
2279
2623
|
if quote:
|
|
2280
2624
|
if ch == "\\" and quote != "'" and i + 1 < n:
|
|
2281
|
-
|
|
2625
|
+
if quote != '"' or not text.startswith(CONTINUATION, i):
|
|
2626
|
+
cur.append(text[i:i + 2])
|
|
2282
2627
|
i += 2
|
|
2283
2628
|
continue
|
|
2284
2629
|
if ch == quote[-1]:
|
|
@@ -2286,17 +2631,19 @@ def _shell_lines(text):
|
|
|
2286
2631
|
elif quote == '"' and (ch == "`" or text.startswith("$(", i)):
|
|
2287
2632
|
runs = True
|
|
2288
2633
|
elif ch == "\\" and i + 1 < n:
|
|
2289
|
-
|
|
2290
|
-
|
|
2634
|
+
if not text.startswith(CONTINUATION, i):
|
|
2635
|
+
cur.append(text[i:i + 2])
|
|
2636
|
+
prev = "_"
|
|
2637
|
+
i += 2
|
|
2291
2638
|
continue
|
|
2292
|
-
elif ch == "#" and
|
|
2639
|
+
elif ch == "#" and _opens_comment(prev):
|
|
2293
2640
|
while i < n and text[i] not in "\r\n":
|
|
2294
2641
|
i += 1
|
|
2295
2642
|
continue
|
|
2296
2643
|
elif ch in "'\"":
|
|
2297
2644
|
quote = "$'" if ch == "'" and cur and cur[-1] == "$" else ch
|
|
2298
2645
|
opened = len(lines)
|
|
2299
|
-
|
|
2646
|
+
prev = "_" if quote or ch in "'\"" else _kept(ch, prev, parens)
|
|
2300
2647
|
cur.append(ch)
|
|
2301
2648
|
i += 1
|
|
2302
2649
|
lines.append("".join(cur))
|
|
@@ -2312,8 +2659,9 @@ def _shell_commands(cmd):
|
|
|
2312
2659
|
A `$( )` or backtick body is a command of its own, ended by `$)`, and the
|
|
2313
2660
|
command around it goes on after its close with a `$()` word in its place."""
|
|
2314
2661
|
out = []
|
|
2315
|
-
text = _blank_heredocs(str(cmd or ""))
|
|
2662
|
+
text = _blank_heredocs(str(cmd or ""))
|
|
2316
2663
|
for line in map(_for_shlex, _shell_lines(text)):
|
|
2664
|
+
line = FD_REDIRECT.sub(r"\2", line)
|
|
2317
2665
|
try:
|
|
2318
2666
|
lex = shlex.shlex(line, posix=True, punctuation_chars=";&|()<>")
|
|
2319
2667
|
lex.whitespace_split = True
|
|
@@ -2328,10 +2676,9 @@ def _shell_commands(cmd):
|
|
|
2328
2676
|
if word == "&" and i < len(words) and REDIRECTION.match(words[i]):
|
|
2329
2677
|
continue # `&>` is a redirection, not a separator
|
|
2330
2678
|
if REDIRECTION.match(word):
|
|
2331
|
-
#
|
|
2332
|
-
#
|
|
2333
|
-
|
|
2334
|
-
cur.pop()
|
|
2679
|
+
# an `&` with its fd (`>&2`), or the target after it - neither is
|
|
2680
|
+
# an argument. The fd number is cut from the line before shlex
|
|
2681
|
+
# (FD_REDIRECT), because `1<` is an fd and `1 <` a program word.
|
|
2335
2682
|
after_redir = True
|
|
2336
2683
|
continue
|
|
2337
2684
|
if after_redir:
|
|
@@ -2339,6 +2686,13 @@ def _shell_commands(cmd):
|
|
|
2339
2686
|
continue # `>&2`: the `&` belongs to the operator
|
|
2340
2687
|
after_redir = False
|
|
2341
2688
|
continue
|
|
2689
|
+
if word.startswith("((") and cur and cur[-1].endswith("$"):
|
|
2690
|
+
# `$(( ))` is arithmetic: its own words are no commands, though
|
|
2691
|
+
# a `$( )` inside one still is and is entered below
|
|
2692
|
+
cur[-1] += "()"
|
|
2693
|
+
subs.append([cur, 1, True])
|
|
2694
|
+
cur = []
|
|
2695
|
+
continue
|
|
2342
2696
|
if not (word and word[0] in ";&|()"):
|
|
2343
2697
|
cur.append(word)
|
|
2344
2698
|
continue
|
|
@@ -2350,21 +2704,32 @@ def _shell_commands(cmd):
|
|
|
2350
2704
|
continue
|
|
2351
2705
|
if subs and piece == "(":
|
|
2352
2706
|
subs[-1][1] += 1
|
|
2707
|
+
if len(subs[-1]) > 2: # a grouping paren inside arithmetic
|
|
2708
|
+
cur = []
|
|
2709
|
+
continue
|
|
2353
2710
|
elif subs and piece == ")":
|
|
2354
2711
|
if not subs[-1][1]:
|
|
2355
|
-
|
|
2712
|
+
arith = len(subs[-1]) > 2
|
|
2713
|
+
if cur and not arith:
|
|
2356
2714
|
out.append([cur, "$)"])
|
|
2357
2715
|
cur = subs.pop()[0]
|
|
2358
2716
|
continue
|
|
2359
2717
|
subs[-1][1] -= 1
|
|
2718
|
+
if len(subs[-1]) > 2: # still the arithmetic's own paren
|
|
2719
|
+
cur = []
|
|
2720
|
+
continue
|
|
2360
2721
|
if cur:
|
|
2361
2722
|
out.append([cur, piece])
|
|
2362
2723
|
elif out:
|
|
2363
2724
|
out[-1][1] += piece
|
|
2364
2725
|
cur = []
|
|
2365
|
-
|
|
2726
|
+
# an arithmetic sub holds no command of its own, only any `$( )` in it,
|
|
2727
|
+
# which was emitted when it closed
|
|
2728
|
+
for seg in [cur] + [sub[0] for sub in reversed(subs) if len(sub) < 3]:
|
|
2366
2729
|
if seg:
|
|
2367
2730
|
out.append([seg, "\n"])
|
|
2731
|
+
elif out: # the line ended on a close: its newline is still a break
|
|
2732
|
+
out[-1][1] += "\n"
|
|
2368
2733
|
if out and out[-1][1] == "\n":
|
|
2369
2734
|
out[-1][1] = ""
|
|
2370
2735
|
return out
|
|
@@ -2374,7 +2739,8 @@ def _shell_segments(cmd):
|
|
|
2374
2739
|
"""The line's simple commands as word lists, read the way
|
|
2375
2740
|
`tezgah_context.shell_programs` reads a line - shlex, posix, punctuation
|
|
2376
2741
|
`;&|()<>` - so quotes and escapes are gone, heredoc bodies are blanked first,
|
|
2377
|
-
a continued line is joined,
|
|
2742
|
+
a continued line is joined (CONTINUATION, in `_shell_lines`, which knows
|
|
2743
|
+
where bash deletes one), a newline ends a command only where bash ends
|
|
2378
2744
|
one (`_shell_lines`), a `$'...'` is one word (`_for_shlex`), and a `$( )` or
|
|
2379
2745
|
backtick body is a command of its own (`_shell_commands`). A line shlex
|
|
2380
2746
|
cannot read is read roughly (`ROUGH_WORDS`) rather than dropped.
|
|
@@ -2639,7 +3005,7 @@ def pipe_hides_status(cmd):
|
|
|
2639
3005
|
OWNING_SEP = re.compile(r"^[()]*(?:&&|\|&?)[()]*$")
|
|
2640
3006
|
|
|
2641
3007
|
|
|
2642
|
-
def status_hidden(cmd):
|
|
3008
|
+
def status_hidden(cmd, cwd=None):
|
|
2643
3009
|
"""True when the line's exit status is not its check's: a pipe owns it
|
|
2644
3010
|
(`pipe_hides_status`), or a command after the check does - the check is
|
|
2645
3011
|
followed by `;` or a newline and more commands, or is sent to the background
|
|
@@ -2648,16 +3014,21 @@ def status_hidden(cmd):
|
|
|
2648
3014
|
status. A heredoc body and its terminator are data here, and an `exit $?`
|
|
2649
3015
|
(or a bare `exit`) right after the check ends the line with the check's
|
|
2650
3016
|
status. Read with `_shell_commands`, each command re-quoted for
|
|
2651
|
-
`verify_command
|
|
2652
|
-
|
|
3017
|
+
`verify_command`; `cwd` reads the repository's declared checks
|
|
3018
|
+
(`repo_checks`) as checks too, so `./bin/run-tool x; echo done` is hidden
|
|
3019
|
+
the way `pytest; echo done` is. ponytail: `set -e` is not read, so `set -e;
|
|
3020
|
+
pytest; echo done` records as ran - a lost credit, never an invented one."""
|
|
2653
3021
|
if pipe_hides_status(cmd):
|
|
2654
3022
|
return True
|
|
2655
3023
|
cmd = str(cmd or "")
|
|
2656
3024
|
body = _heredoc_bytes(cmd)
|
|
2657
3025
|
cmds = _shell_commands("".join(" " if b and ch != "\n" else ch
|
|
2658
3026
|
for ch, b in zip(cmd, body)))
|
|
3027
|
+
prefixes = repo_checks(cwd)
|
|
2659
3028
|
for i, (words, _sep) in enumerate(cmds):
|
|
2660
|
-
|
|
3029
|
+
line = shlex.join(words)
|
|
3030
|
+
if not (verify_command(line) or any(
|
|
3031
|
+
mode == "check" for _n, mode in _repo_check_runs(line, prefixes))):
|
|
2661
3032
|
continue
|
|
2662
3033
|
for k in range(i, len(cmds) - 1):
|
|
2663
3034
|
if OWNING_SEP.match(cmds[k][1]):
|
|
@@ -2685,9 +3056,10 @@ def piped_check(cmd):
|
|
|
2685
3056
|
c = mask(cmd)
|
|
2686
3057
|
if PIPEFAIL.match(c):
|
|
2687
3058
|
return None
|
|
2688
|
-
raw =
|
|
3059
|
+
raw = joined(cmd)
|
|
2689
3060
|
pos, check = 0, None
|
|
2690
|
-
# mask keeps every offset
|
|
3061
|
+
# mask keeps every offset of `joined`, so a stage's span in `c` is its text
|
|
3062
|
+
# in `raw`
|
|
2691
3063
|
for part in re.split(r"(&&|\|\||;|\n|\|)", c):
|
|
2692
3064
|
if part in ("&&", "||", ";", "\n"):
|
|
2693
3065
|
check = None
|
|
@@ -2793,14 +3165,15 @@ def shortcut_edit(inp):
|
|
|
2793
3165
|
return None
|
|
2794
3166
|
|
|
2795
3167
|
|
|
2796
|
-
def classify(tool, inp):
|
|
2797
|
-
"""The evidence kind for a tool call, or None. Shared by the host hooks.
|
|
3168
|
+
def classify(tool, inp, cwd=None):
|
|
3169
|
+
"""The evidence kind for a tool call, or None. Shared by the host hooks.
|
|
3170
|
+
`cwd` lets the repository's declared checks (`repo_checks`) count."""
|
|
2798
3171
|
t = str(tool or "").lower()
|
|
2799
3172
|
if t in WRITE_TOOLS:
|
|
2800
3173
|
return "edit"
|
|
2801
3174
|
if t in BASH_TOOLS:
|
|
2802
3175
|
cmd = inp.get("command") or inp.get("cmd") or ""
|
|
2803
|
-
return "verify" if verify_command(cmd) else "run"
|
|
3176
|
+
return "verify" if verify_command(cmd, cwd) else "run"
|
|
2804
3177
|
return None
|
|
2805
3178
|
|
|
2806
3179
|
|
|
@@ -2990,7 +3363,12 @@ TIER_LOCAL_ARGS = ("-h", "--help", "--use")
|
|
|
2990
3363
|
UNTRUSTED_CHANNEL = {"web": "a web result", "mcp": "an MCP server",
|
|
2991
3364
|
"network": "a network read",
|
|
2992
3365
|
"tier": "an external model answer",
|
|
2993
|
-
"subagent": "a subagent's report"
|
|
3366
|
+
"subagent": "a subagent's report",
|
|
3367
|
+
"repo": "a tool this repository declares untrusted"}
|
|
3368
|
+
# The sixth channel is the repository's own: its manifest's `untrusted` list
|
|
3369
|
+
# names programs whose output is someone else's text - a scanner reading a
|
|
3370
|
+
# target, a fetcher this file cannot know (`tezgah_extension.untrusted_programs`).
|
|
3371
|
+
REPO_CHANNEL = "repo"
|
|
2994
3372
|
# A delegate's report is the fifth channel, and the one no host counted: what a
|
|
2995
3373
|
# subagent hands back is prose this session did not write and cannot vouch for,
|
|
2996
3374
|
# read by a parent that never watched it being produced. Spelled the way every
|
|
@@ -3018,14 +3396,35 @@ def _tier_read(cmd):
|
|
|
3018
3396
|
return False
|
|
3019
3397
|
|
|
3020
3398
|
|
|
3021
|
-
def
|
|
3399
|
+
def _repo_untrusted(cmd, cwd):
|
|
3400
|
+
"""True when this shell line runs a program the repository at `cwd` (None:
|
|
3401
|
+
the hook process's directory) declares untrusted. Read at a command
|
|
3402
|
+
position by `shell_programs`, so a mention in an argument is not a run.
|
|
3403
|
+
Imported here for `_tier_read`'s reason; a broken extension, or a process
|
|
3404
|
+
directory that is gone, costs the channel, never the call."""
|
|
3405
|
+
try:
|
|
3406
|
+
import tezgah_extension
|
|
3407
|
+
from tezgah_context import shell_programs
|
|
3408
|
+
names = tezgah_extension.untrusted_programs(cwd or os.getcwd())
|
|
3409
|
+
return bool(names) and bool(names.intersection(shell_programs(cmd)))
|
|
3410
|
+
except Exception:
|
|
3411
|
+
return False
|
|
3412
|
+
|
|
3413
|
+
|
|
3414
|
+
def untrusted_source(tool, inp, cwd=None):
|
|
3022
3415
|
"""The untrusted channel this call's result came through, or None.
|
|
3023
3416
|
|
|
3024
3417
|
Decided from the call, because that is all a PostToolUse hook sees: the
|
|
3025
3418
|
tool's name for the two named channels, the masked command text for a shell
|
|
3026
3419
|
read that left the machine. None means the result is the user's or this
|
|
3027
3420
|
workspace's, which is the normal case - the label names the exception, so it
|
|
3028
|
-
never becomes noise the model learns to skip.
|
|
3421
|
+
never becomes noise the model learns to skip.
|
|
3422
|
+
|
|
3423
|
+
`cwd` is where the repository's `untrusted` declaration is read from; None
|
|
3424
|
+
reads the call's own `cwd`/`workdir`, else the hook process's directory.
|
|
3425
|
+
ponytail: the callers pass no cwd today, so a host that runs its hook
|
|
3426
|
+
elsewhere than the session reads no declaration - a missed label, never a
|
|
3427
|
+
false one."""
|
|
3029
3428
|
name = str(tool or "").strip().lower()
|
|
3030
3429
|
if MCP_TOOL.match(name):
|
|
3031
3430
|
return "mcp"
|
|
@@ -3039,6 +3438,9 @@ def untrusted_source(tool, inp):
|
|
|
3039
3438
|
return "network"
|
|
3040
3439
|
if name in BASH_TOOLS and _tier_read(cmd):
|
|
3041
3440
|
return "tier"
|
|
3441
|
+
if name in BASH_TOOLS and cmd and _repo_untrusted(
|
|
3442
|
+
cmd, cwd or inp.get("cwd") or inp.get("workdir")):
|
|
3443
|
+
return REPO_CHANNEL
|
|
3042
3444
|
return None
|
|
3043
3445
|
|
|
3044
3446
|
|
|
@@ -3380,9 +3782,9 @@ def note_tool(session_id, tool, inp, failed=None, *, interrupted=False,
|
|
|
3380
3782
|
workspace=root_for(cwd) if cwd else None)
|
|
3381
3783
|
return
|
|
3382
3784
|
cmd = str(inp.get("command") or inp.get("cmd") or "")
|
|
3383
|
-
kind = classify(tool, inp)
|
|
3785
|
+
kind = classify(tool, inp, cwd)
|
|
3384
3786
|
if kind == "verify":
|
|
3385
|
-
if failed is None or status_hidden(cmd) or (empty_run and not failed):
|
|
3787
|
+
if failed is None or status_hidden(cmd, cwd) or (empty_run and not failed):
|
|
3386
3788
|
kind = "verify"
|
|
3387
3789
|
else:
|
|
3388
3790
|
kind = "verify_fail" if failed else "verify_ok"
|
|
@@ -3692,7 +4094,15 @@ def passing_check(entry):
|
|
|
3692
4094
|
return False
|
|
3693
4095
|
if entry.get(ORPHAN) or entry.get("exit") != 0 or entry.get("out_bytes") == 0:
|
|
3694
4096
|
return False
|
|
3695
|
-
|
|
4097
|
+
# The row carries no cwd: its one location is `repo`, which `_check_repo`
|
|
4098
|
+
# sets only when the call named where it ran (a `cwd`/`workdir` argument, a
|
|
4099
|
+
# leading `cd X`, path arguments in one repository) and leaves None
|
|
4100
|
+
# otherwise. So this re-read sees the repository's declared checks
|
|
4101
|
+
# (`repo_checks`) only on such a row; on the rest it reads the built-in
|
|
4102
|
+
# vocabulary alone. The reading that holds `./bin/run-tool x; echo done` to
|
|
4103
|
+
# `verify` is `note_tool`'s, with the call's own cwd - this one guards a
|
|
4104
|
+
# `verify_ok` row another writer appended, as far as its `repo` reaches.
|
|
4105
|
+
return not status_hidden(entry.get("detail"), entry.get("repo"))
|
|
3696
4106
|
|
|
3697
4107
|
|
|
3698
4108
|
def _changed_write(row):
|
|
@@ -3949,7 +4359,26 @@ def _ui_evidence(rows):
|
|
|
3949
4359
|
return write, proof
|
|
3950
4360
|
|
|
3951
4361
|
|
|
3952
|
-
def
|
|
4362
|
+
def _slop_row_holds(row, cwd):
|
|
4363
|
+
"""True when a `tezgah-design slop` row is evidence for THIS repository.
|
|
4364
|
+
|
|
4365
|
+
The verdict is a file, so the command alone is not enough: a scan pointed at
|
|
4366
|
+
another tree (`--repo <fixture>`) would pass a component edit it never looked
|
|
4367
|
+
at (review of plan 133, finding 5). The repo the row names, or the row's own
|
|
4368
|
+
cwd when it names none, has to be the session's repository."""
|
|
4369
|
+
detail = mask(str(row.get("detail") or ""))
|
|
4370
|
+
named = SLOP_REPO.search(detail)
|
|
4371
|
+
target = named.group(1).strip("'\"") if named else ""
|
|
4372
|
+
base = os.path.realpath(os.path.abspath(cwd or os.getcwd()))
|
|
4373
|
+
root = _toplevel(base) or base
|
|
4374
|
+
want = os.path.realpath(root)
|
|
4375
|
+
if not target or target in (".", "./"):
|
|
4376
|
+
return True
|
|
4377
|
+
path = os.path.realpath(os.path.join(root, os.path.expanduser(target)))
|
|
4378
|
+
return path == want or path.startswith(want + os.sep)
|
|
4379
|
+
|
|
4380
|
+
|
|
4381
|
+
def _design_evidence(rows, cwd=None):
|
|
3953
4382
|
"""(component, check) as row indices for the design-contract half of the UI
|
|
3954
4383
|
rule, -1 for either when the row is not there.
|
|
3955
4384
|
|
|
@@ -3966,6 +4395,8 @@ def _design_evidence(rows):
|
|
|
3966
4395
|
if _ui_write(row) and DESIGN_COMPONENT.search(detail):
|
|
3967
4396
|
component = i
|
|
3968
4397
|
if passing_check(row) and DESIGN_CHECK.search(detail):
|
|
4398
|
+
if DESIGN_SLOP.search(detail) and not _slop_row_holds(row, cwd):
|
|
4399
|
+
continue
|
|
3969
4400
|
check = i
|
|
3970
4401
|
return component, check
|
|
3971
4402
|
|
|
@@ -4578,6 +5009,12 @@ GIT_BOOKKEEPING = frozenset((
|
|
|
4578
5009
|
# is work - an unknown program owes the check, a known one never excuses a write.
|
|
4579
5010
|
GIT_READS = GIT_BOOKKEEPING - {"add", "commit", "push", "fetch", "branch", "tag",
|
|
4580
5011
|
"remote"}
|
|
5012
|
+
# `set`'s flags that only change when the shell stops or what it traces: the
|
|
5013
|
+
# `set -o pipefail;` prefix the contract asks a check line for. Anything else
|
|
5014
|
+
# is no read: `-k` and `+o interactive-comments` change how the rest of the
|
|
5015
|
+
# line runs, a plain word sets the positional parameters.
|
|
5016
|
+
SET_FLAGS = re.compile(r"[-+][euxvo]+")
|
|
5017
|
+
SET_NAMES = frozenset(("pipefail", "errexit", "nounset", "xtrace", "verbose"))
|
|
4581
5018
|
# ssh's options that take the next word, so the host is read after them
|
|
4582
5019
|
SSH_VALUE_OPTS = frozenset(("-b", "-c", "-D", "-E", "-e", "-F", "-I", "-i", "-J",
|
|
4583
5020
|
"-L", "-l", "-m", "-O", "-o", "-p", "-Q", "-R", "-S",
|
|
@@ -4607,6 +5044,7 @@ SUBCOMMAND_READS = {"gh": (("pr", "view"), ("pr", "list"), ("issue", "view"),
|
|
|
4607
5044
|
# all (review F6), as a pattern read at an argument's start: git's `--output`
|
|
4608
5045
|
# and external diff/textconv drivers, ripgrep's `--pre`, find's actions, a
|
|
4609
5046
|
# short-option cluster holding sort's `-o`, sed's `-i`/`-f`, awk's `-i`/`-f`,
|
|
5047
|
+
# printf's `-v` (it assigns a variable, PATH included: plan 126),
|
|
4610
5048
|
# and curl's writes to a file and non-GET requests. `tree` and `file` left the
|
|
4611
5049
|
# list instead of growing one here: tree writes through clustered short options
|
|
4612
5050
|
# (`-fo out`) and `-R -H`, file through `-C` (review N4).
|
|
@@ -4615,6 +5053,7 @@ WRITING_OPTIONS = {
|
|
|
4615
5053
|
"rg": re.compile(r"--pre"),
|
|
4616
5054
|
"find": re.compile(r"-(?:delete|exec|execdir|ok|okdir|fprint0?|fprintf|fls)$"),
|
|
4617
5055
|
"sort": re.compile(r"-[^-]*o|--output"),
|
|
5056
|
+
"printf": re.compile(r"-v"),
|
|
4618
5057
|
"sed": re.compile(r"-[^-]*[if]|--(?:in-place|file)"),
|
|
4619
5058
|
"awk": re.compile(r"-[^-]*[if]|--(?:include|file|load)"),
|
|
4620
5059
|
"gawk": re.compile(r"-[^-]*[if]|--(?:include|file|load|inplace)"),
|
|
@@ -4626,9 +5065,32 @@ WRITING_OPTIONS = {
|
|
|
4626
5065
|
GIT_CONFIG_OPTS = ("-c", "--config-env", "--exec-path")
|
|
4627
5066
|
# an fd duplication or a discard: a redirect that writes no file
|
|
4628
5067
|
HARMLESS_REDIRECT = re.compile(r"\d*>&\d+|\d*>>?\s*/dev/null\b")
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
5068
|
+
# The compound-command words `loops=True` reads (plan 126): an opener is no
|
|
5069
|
+
# program (the command after it is: `do cat "$f"`), a closer runs nothing
|
|
5070
|
+
SHELL_OPENERS = frozenset(("if", "then", "elif", "else", "do", "while", "until",
|
|
5071
|
+
"!", "{"))
|
|
5072
|
+
SHELL_CLOSERS = frozenset(("done", "fi", "}"))
|
|
5073
|
+
# a loop variable no shell reads: one lowercase letter. A longer lowercase
|
|
5074
|
+
# name can be one zsh ties to a setting (`path` is PATH, and `cdpath`, `fpath`,
|
|
5075
|
+
# `manpath`, `module_path`, `psvar`, `watch`, `histchars`, `prompt`, `status`
|
|
5076
|
+
# and the modules' `options`, `mapfile`... are its own), and its list grows
|
|
5077
|
+
# with every loaded module
|
|
5078
|
+
LOOP_NAME = re.compile(r"[a-z]")
|
|
5079
|
+
# A word this reader passes on: bash expands neither a brace list nor an
|
|
5080
|
+
# operator form to anything it can read. `{-delete,-print}` is two words,
|
|
5081
|
+
# `${x:--delete}` an option, `$[x]`, `$((x))` and `${a[x]}` evaluate x as
|
|
5082
|
+
# arithmetic, which runs a `$( )` in its value. Read on `_shell_read`'s masks.
|
|
5083
|
+
BRACE_EXPANSION = re.compile(r"\{[^\s;|&<>()]*(?:,|\.\.)[^\s;|&<>()]*\}")
|
|
5084
|
+
PLAIN_DOLLAR = re.compile(r"\$(?:[A-Za-z_]\w*|\{(?:[A-Za-z_]\w*|[0-9@*#?$!-])\}|"
|
|
5085
|
+
r"[0-9@*#?$!-]|(?![\[{(]))")
|
|
5086
|
+
# a function's arguments: values this reader never sees
|
|
5087
|
+
POSITIONAL = re.compile(r"\$\{?[0-9@*#]")
|
|
5088
|
+
# the readers no option of which writes or runs: the only ones a value this
|
|
5089
|
+
# reader never sees (a loop word, a substitution's output) may reach
|
|
5090
|
+
OPTION_FREE = READ_ONLY_PROGRAMS - set(WRITING_OPTIONS)
|
|
5091
|
+
|
|
5092
|
+
|
|
5093
|
+
def _shell_read(cmd, subst=False):
|
|
4632
5094
|
"""(effect, text) for `cmd`: whether bash would run a command or process
|
|
4633
5095
|
substitution, or redirect into a file, anywhere in it - or this reader
|
|
4634
5096
|
cannot tell - and the command with what is not a command blanked for
|
|
@@ -4645,7 +5107,16 @@ def _shell_read(cmd):
|
|
|
4645
5107
|
|
|
4646
5108
|
Fails closed (effect True): an unquoted heredoc expands `$(` in its body,
|
|
4647
5109
|
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).
|
|
5110
|
+
open at the end means the reading lost sync with bash (review N1).
|
|
5111
|
+
|
|
5112
|
+
Also an effect: a brace expansion (unquoted `{a,b}`), a function definition
|
|
5113
|
+
(`name()`: its body runs with arguments read nowhere), and a word whose `$`
|
|
5114
|
+
is more than a plain `$name`/`${name}` or holds a `[` (`PLAIN_DOLLAR`;
|
|
5115
|
+
double quotes still expand it, single quotes do not).
|
|
5116
|
+
|
|
5117
|
+
`subst=True` reads an unquoted `$(` as no effect of its own: its body is a
|
|
5118
|
+
command `_shell_segments` returns, and its redirects are in the bare text;
|
|
5119
|
+
inside double quotes it stays one word that reader never splits."""
|
|
4649
5120
|
n, out = len(cmd), list(cmd)
|
|
4650
5121
|
|
|
4651
5122
|
def blank(a, b):
|
|
@@ -4659,7 +5130,8 @@ def _shell_read(cmd):
|
|
|
4659
5130
|
blank(start, stop)
|
|
4660
5131
|
blank(body, term[1])
|
|
4661
5132
|
cmd = "".join(out)
|
|
4662
|
-
bare, quote, i = [], None, 0
|
|
5133
|
+
bare, lit, dq, quote, i = [], [], [], None, 0
|
|
5134
|
+
kept, parens = "", [] # lit's last character as `_opens_comment` asks it
|
|
4663
5135
|
while i < n:
|
|
4664
5136
|
ch = cmd[i]
|
|
4665
5137
|
if quote == "'":
|
|
@@ -4670,14 +5142,25 @@ def _shell_read(cmd):
|
|
|
4670
5142
|
elif ch == "'":
|
|
4671
5143
|
quote = None
|
|
4672
5144
|
elif ch == "\\":
|
|
5145
|
+
# a CONTINUATION: bash deletes both, so `$\<nl>1` is `$1` and
|
|
5146
|
+
# nothing is left in the word
|
|
5147
|
+
if cmd.startswith(CONTINUATION, i):
|
|
5148
|
+
i += 2
|
|
5149
|
+
continue
|
|
4673
5150
|
i += 2
|
|
4674
5151
|
bare.append(" ")
|
|
5152
|
+
lit.append("_")
|
|
5153
|
+
dq.append("_")
|
|
5154
|
+
kept = "_"
|
|
4675
5155
|
continue
|
|
4676
|
-
elif ch == "`" or cmd.startswith("$(", i):
|
|
5156
|
+
elif ch == "`" or (cmd.startswith("$(", i) and (quote or not subst)):
|
|
4677
5157
|
return True, ""
|
|
4678
5158
|
elif quote == '"':
|
|
4679
5159
|
quote = None if ch == '"' else quote
|
|
4680
|
-
|
|
5160
|
+
# a `#` opens a comment only at a word's start (`_opens_comment`): after
|
|
5161
|
+
# an escaped or quoted character (`x\ #...`) or a substitution's `)`
|
|
5162
|
+
# (`_kept`) it is part of the word
|
|
5163
|
+
elif ch == "#" and _opens_comment(kept):
|
|
4681
5164
|
end = cmd.find("\n", i)
|
|
4682
5165
|
end = n if end < 0 else end
|
|
4683
5166
|
blank(i, end)
|
|
@@ -4690,19 +5173,53 @@ def _shell_read(cmd):
|
|
|
4690
5173
|
quote = ch
|
|
4691
5174
|
else:
|
|
4692
5175
|
bare.append(ch)
|
|
5176
|
+
lit.append(ch)
|
|
5177
|
+
dq.append(ch)
|
|
5178
|
+
kept = _kept(ch, kept, parens)
|
|
4693
5179
|
i += 1
|
|
4694
5180
|
continue
|
|
4695
5181
|
bare.append(" ")
|
|
5182
|
+
# `lit` keeps only unquoted text, `dq` double-quoted text too
|
|
5183
|
+
lit.append("_")
|
|
5184
|
+
kept = "_"
|
|
5185
|
+
dq.append(ch if quote == '"' or ch == '"' and not quote else "_")
|
|
4696
5186
|
i += 1
|
|
4697
5187
|
if quote:
|
|
4698
5188
|
return True, ""
|
|
5189
|
+
lit, dq = "".join(lit), "".join(dq)
|
|
5190
|
+
if BRACE_EXPANSION.search(lit) or re.search(r"\(\s*\)", lit) or "$((" in dq:
|
|
5191
|
+
return True, ""
|
|
5192
|
+
for word in re.split(r"[\s;&|()<>]+", dq):
|
|
5193
|
+
if "$" in word and ("[" in word or "$" in PLAIN_DOLLAR.sub("", word)):
|
|
5194
|
+
return True, ""
|
|
5195
|
+
# a positional parameter is a value no read here sees: where loops read
|
|
5196
|
+
# (`subst`), a `$1` the shell expands - unquoted or double-quoted, never one
|
|
5197
|
+
# inside single quotes, a comment or a heredoc body - makes the command work
|
|
5198
|
+
if subst and POSITIONAL.search(dq):
|
|
5199
|
+
return True, ""
|
|
4699
5200
|
text = HARMLESS_REDIRECT.sub(" ", "".join(bare))
|
|
4700
5201
|
# every `<` left is an input redirect or a here-string, both reads; blanking
|
|
4701
5202
|
# them keeps `_shell_segments`' own heredoc pass from reading one again
|
|
4702
5203
|
return ">" in text or "<(" in text, "".join(out).replace("<", " ")
|
|
4703
5204
|
|
|
4704
5205
|
|
|
4705
|
-
def
|
|
5206
|
+
def _set_options(args):
|
|
5207
|
+
"""True when `set`'s words are only SET_FLAGS, each `o` in them followed by
|
|
5208
|
+
a SET_NAMES name or by nothing (`set -o` prints the options). A bare `set`
|
|
5209
|
+
prints every variable, and stays work like any other unlisted shape."""
|
|
5210
|
+
j = 0
|
|
5211
|
+
while j < len(args):
|
|
5212
|
+
if not SET_FLAGS.fullmatch(args[j]):
|
|
5213
|
+
return False
|
|
5214
|
+
for _ in range(args[j].count("o")):
|
|
5215
|
+
j += 1
|
|
5216
|
+
if j < len(args) and args[j] not in SET_NAMES:
|
|
5217
|
+
return False
|
|
5218
|
+
j += 1
|
|
5219
|
+
return bool(args)
|
|
5220
|
+
|
|
5221
|
+
|
|
5222
|
+
def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING, loops=False):
|
|
4706
5223
|
"""True when every simple command of `cmd` reads, or runs a git subcommand
|
|
4707
5224
|
in `git` (VCS bookkeeping that changes no working-tree file by default,
|
|
4708
5225
|
GIT_READS for a pure read). A redirect, a substitution, an env assignment
|
|
@@ -4713,21 +5230,40 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4713
5230
|
`codegraph` with a read verb (`SUBCOMMAND_READS`). `sqlite3` reads with a
|
|
4714
5231
|
read-only open (the option, or `mode=ro` in the database URI) and no
|
|
4715
5232
|
writing dot-command or stdin (`SQLITE_WRITE`), `ssh` with a remote command
|
|
4716
|
-
that is itself one of these reads
|
|
5233
|
+
that is itself one of these reads, `set` with shell options only
|
|
5234
|
+
(`_set_options`).
|
|
5235
|
+
|
|
5236
|
+
`loops=True` (plan 126) also reads a `for NAME in WORDS` loop, the
|
|
5237
|
+
`if`/`while`/`{` openers and closers around reads, and an unquoted `$( )`
|
|
5238
|
+
whose body reads. The values those bind - a loop word, a substitution's
|
|
5239
|
+
output, a positional parameter (`POSITIONAL`) - are text this reader never
|
|
5240
|
+
sees, so in such a command a `$` word may only reach a reader no option of
|
|
5241
|
+
which writes (`OPTION_FREE`), and the loop variable is one letter no shell
|
|
5242
|
+
reads (`LOOP_NAME`).
|
|
4717
5243
|
|
|
4718
5244
|
ponytail: `git commit` and `git push` run the repository's hooks, and a
|
|
4719
5245
|
pre-commit autofixer can rewrite files the row never names. They stay on
|
|
4720
5246
|
the list because refusing every commit was the measured cost (audit CHAT-04);
|
|
4721
5247
|
a hook that rewrites the tree is invisible here."""
|
|
4722
5248
|
raw = str(cmd or "").replace(FAILED_MARK, "")
|
|
4723
|
-
effect, text = _shell_read(raw) if raw.strip() else (True, "")
|
|
5249
|
+
effect, text = _shell_read(raw, loops) if raw.strip() else (True, "")
|
|
4724
5250
|
if effect:
|
|
4725
5251
|
return False
|
|
4726
5252
|
segs = _shell_segments(text)
|
|
5253
|
+
unseen = loops and any("for" in w or any("$(" in a for a in w) for w in segs)
|
|
4727
5254
|
for words in segs:
|
|
4728
5255
|
i = 0
|
|
4729
|
-
while i < len(words) and words[i] in GIT_WRAPPER
|
|
5256
|
+
while i < len(words) and (words[i] in GIT_WRAPPER
|
|
5257
|
+
or loops and words[i] in SHELL_OPENERS):
|
|
4730
5258
|
i += 1
|
|
5259
|
+
if loops and all(w in SHELL_OPENERS for w in words[:i]) and (
|
|
5260
|
+
i == len(words) or words[i:] == words[-1:] and words[i] in SHELL_CLOSERS):
|
|
5261
|
+
continue
|
|
5262
|
+
if loops and words[i:i + 1] == ["for"]:
|
|
5263
|
+
if not (words[i + 1:] and LOOP_NAME.fullmatch(words[i + 1])
|
|
5264
|
+
and words[i + 2:i + 3] in ([], ["in"])):
|
|
5265
|
+
return False
|
|
5266
|
+
continue
|
|
4731
5267
|
if i >= len(words) or ENV_WORD.match(words[i]):
|
|
4732
5268
|
return False
|
|
4733
5269
|
# a program named by a path is whatever that file is, not the
|
|
@@ -4735,6 +5271,8 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4735
5271
|
if "/" in words[i]:
|
|
4736
5272
|
return False
|
|
4737
5273
|
program, args = words[i], words[i + 1:]
|
|
5274
|
+
if unseen and program not in OPTION_FREE and any("$" in a for a in args):
|
|
5275
|
+
return False
|
|
4738
5276
|
writes = WRITING_OPTIONS.get(program)
|
|
4739
5277
|
if writes and any(writes.match(a) for a in args):
|
|
4740
5278
|
return False
|
|
@@ -4745,6 +5283,10 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4745
5283
|
if any(SCRIPT_WRITE[program].search(a) for a in plain):
|
|
4746
5284
|
return False
|
|
4747
5285
|
continue
|
|
5286
|
+
if program == "set":
|
|
5287
|
+
if not _set_options(args):
|
|
5288
|
+
return False
|
|
5289
|
+
continue
|
|
4748
5290
|
if program == "uniq":
|
|
4749
5291
|
# `uniq IN OUT` writes its second operand
|
|
4750
5292
|
if len(plain) > 1:
|
|
@@ -4786,7 +5328,7 @@ def _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
|
|
|
4786
5328
|
return bool(segs)
|
|
4787
5329
|
|
|
4788
5330
|
|
|
4789
|
-
def _bookkeeping_turn(rows, git=GIT_BOOKKEEPING):
|
|
5331
|
+
def _bookkeeping_turn(rows, git=GIT_BOOKKEEPING, loops=False):
|
|
4790
5332
|
"""True when every work row in `rows` is a shell call that read or ran a
|
|
4791
5333
|
git subcommand in `git` (`_bookkeeping_command`) and was not seen to
|
|
4792
5334
|
change a file.
|
|
@@ -4800,7 +5342,7 @@ def _bookkeeping_turn(rows, git=GIT_BOOKKEEPING):
|
|
|
4800
5342
|
r.get("kind") == "run" and not _change_row(r)
|
|
4801
5343
|
and len(str(r.get("detail") or "")) < DETAIL_MAX
|
|
4802
5344
|
and "[redacted:" not in str(r.get("detail") or "")
|
|
4803
|
-
and _bookkeeping_command(r.get("detail"), git) for r in steps)
|
|
5345
|
+
and _bookkeeping_command(r.get("detail"), git, loops) for r in steps)
|
|
4804
5346
|
|
|
4805
5347
|
|
|
4806
5348
|
def _settled(rows):
|
|
@@ -4915,7 +5457,12 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4915
5457
|
# `tail` or a read-only query owes no check. Its claim words are read the
|
|
4916
5458
|
# same way, and a claim is judged against the session's earlier turns below.
|
|
4917
5459
|
quiet = _bookkeeping_turn(rows, GIT_READS)
|
|
4918
|
-
|
|
5460
|
+
# A turn of reads that loop over files or read a read's output (plan 126:
|
|
5461
|
+
# `for f in $(find src); do cat -n "$f"; done`, refused in 4 of 6 live
|
|
5462
|
+
# advice turns) ends like one with no work - but only on a reply that
|
|
5463
|
+
# claims nothing. A done/tested claim over it keeps the worked turn's fold.
|
|
5464
|
+
read = quiet or _bookkeeping_turn(rows, GIT_READS, loops=True)
|
|
5465
|
+
if not worked or read:
|
|
4919
5466
|
# With no work, the words are the whole trigger, so a question or a
|
|
4920
5467
|
# negation is not read as the claim it names (`asserted_claims`).
|
|
4921
5468
|
done, verified = asserted_claims(t)
|
|
@@ -4939,7 +5486,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
4939
5486
|
# exit below - "nothing was done and nothing was claimed" - is exactly the
|
|
4940
5487
|
# shape that used to leave such a claim unjudged.
|
|
4941
5488
|
external = _external_claim(t)
|
|
4942
|
-
if (not worked or
|
|
5489
|
+
if (not worked or read) and not (done or verified) and not external:
|
|
4943
5490
|
return _residual_block(t, rows, shape, session_id=session_id, cwd=cwd)
|
|
4944
5491
|
if lost:
|
|
4945
5492
|
return _residual_block(t, rows, shape,
|
|
@@ -5070,12 +5617,12 @@ def _evidence_text(cls, rows, external, where="this turn"):
|
|
|
5070
5617
|
% (where, shown or "a UI file"))
|
|
5071
5618
|
return ("no ui_ok",
|
|
5072
5619
|
"Design contract: %s changed a component (%s) and no "
|
|
5073
|
-
"
|
|
5620
|
+
"`~/.config/tezgah/bin/tezgah-design check` ran over a measurement, so nothing "
|
|
5074
5621
|
"says the component is on the repository's floor - a read of "
|
|
5075
5622
|
"the screen says what it looks like, not whether it is on "
|
|
5076
|
-
"that floor. Run
|
|
5623
|
+
"that floor. Run `~/.config/tezgah/bin/tezgah-design check --contract "
|
|
5077
5624
|
".tezgah/design-contract.md --measured <measurement.json>` "
|
|
5078
|
-
"(
|
|
5625
|
+
"(`~/.config/tezgah/bin/tezgah-design derive` writes the contract when the repo "
|
|
5079
5626
|
"has none; the measurement is the per-component styles and "
|
|
5080
5627
|
"states the analyze-app loop produces) and report every "
|
|
5081
5628
|
"violation it prints; if the check cannot run here, write "
|
|
@@ -5487,9 +6034,9 @@ RESIDUAL_WORK = (
|
|
|
5487
6034
|
# twice more.
|
|
5488
6035
|
STOPPED_SLICE = (
|
|
5489
6036
|
"{SLICE_LEAD}Slice %s, spawned by this session, stopped (%s) and "
|
|
5490
|
-
"its work is not resumed. Resume it with
|
|
6037
|
+
"its work is not resumed. Resume it with `~/.config/tezgah/bin/tezgah-task slice resume %s` - "
|
|
5491
6038
|
"the native path first, else the re-dispatch brief it prints - or, if the "
|
|
5492
|
-
"work was finished another way, record that with
|
|
6039
|
+
"work was finished another way, record that with `~/.config/tezgah/bin/tezgah-task slice done "
|
|
5493
6040
|
"%s`.")
|
|
5494
6041
|
|
|
5495
6042
|
# The `ready steps` class (plan 086, S1): the newest todo list (`note_todo`)
|