@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.
Files changed (426) hide show
  1. package/CHANGELOG.md +83 -1
  2. package/MANIFEST +382 -144
  3. package/README.de.md +1 -1
  4. package/README.es.md +1 -1
  5. package/README.fr.md +1 -1
  6. package/README.md +1 -1
  7. package/README.tr.md +1 -1
  8. package/bin/codegen +5 -2
  9. package/bin/tezgah-design +288 -1
  10. package/bin/tezgah-docs +1 -1
  11. package/bin/tezgah-doctor +39 -2
  12. package/bin/tezgah-import-ai-research +26 -12
  13. package/bin/tezgah-ocr-review +296 -0
  14. package/bin/tezgah-research +216 -40
  15. package/bin/tezgah-review-quote +185 -0
  16. package/bin/tezgah-setup +136 -33
  17. package/bin/tezgah-skill +1 -1
  18. package/bin/tezgah-status +201 -3
  19. package/bin/tezgah-task +203 -3
  20. package/commands/adhd.md +2 -2
  21. package/commands/ponytail.md +4 -4
  22. package/docs/architecture.md +2 -2
  23. package/docs/citations-baseline.json +2 -2
  24. package/docs/clarity-baseline.json +2 -2
  25. package/docs/contract.md +87 -42
  26. package/docs/environment.md +2 -0
  27. package/docs/evidence.md +28 -9
  28. package/docs/feature-audit.md +2 -2
  29. package/docs/gate.md +54 -31
  30. package/docs/glossary.md +5 -2
  31. package/docs/hosts.md +17 -3
  32. package/docs/index.json +34 -8
  33. package/docs/judge.md +4 -2
  34. package/docs/layers.md +3 -3
  35. package/docs/models.md +31 -9
  36. package/docs/operations.md +126 -1
  37. package/docs/research.md +786 -5
  38. package/docs/skills.md +137 -13
  39. package/docs/status-line.md +115 -1
  40. package/hooks/hooks.json +1 -1
  41. package/hooks/lang/en.json +10 -0
  42. package/hooks/lang/tr.json +11 -1
  43. package/hooks/tezgah_agents.py +38 -22
  44. package/hooks/tezgah_apps.py +18 -0
  45. package/hooks/tezgah_components.py +4 -0
  46. package/hooks/tezgah_context.py +189 -50
  47. package/hooks/tezgah_extension.py +337 -0
  48. package/hooks/tezgah_gate.py +188 -56
  49. package/hooks/tezgah_integrity.py +652 -105
  50. package/hooks/tezgah_judge.py +6 -4
  51. package/hooks/tezgah_models.py +59 -19
  52. package/hooks/tezgah_orca.py +1 -1
  53. package/hooks/tezgah_paths.py +102 -6
  54. package/hooks/tezgah_policy.py +94 -45
  55. package/hooks/tezgah_rank.py +9 -6
  56. package/hooks/tezgah_replay.py +4 -4
  57. package/hooks/tezgah_research.py +1260 -65
  58. package/hooks/tezgah_research_capture.py +1544 -0
  59. package/hooks/tezgah_research_evidence.py +842 -0
  60. package/hooks/tezgah_research_stats.py +258 -0
  61. package/hooks/tezgah_skill_pick.py +408 -106
  62. package/hooks/tezgah_stopspec.py +8 -1
  63. package/hooks/tezgah_task.py +12 -4
  64. package/hooks/tezgah_taste_ledger.py +1 -1
  65. package/hosts/cursor/hook.py +29 -2
  66. package/hosts/opencode/plugins/tezgah.js +307 -46
  67. package/hosts/opencode/tui/cli/tui.tsx +100 -0
  68. package/hosts/opencode/tui/tezgah-tui.tsx +2 -1
  69. package/output-styles/tezgah.md +10 -10
  70. package/package.json +1 -1
  71. package/skills/ai-research/SKILL.md +21 -14
  72. package/skills/ai-research/SOURCE +7 -5
  73. package/skills/ai-research/index/1-frame.md +6 -6
  74. package/skills/ai-research/index/2-data.md +9 -9
  75. package/skills/ai-research/index/3-train.md +36 -36
  76. package/skills/ai-research/index/4-measure.md +13 -13
  77. package/skills/ai-research/index/5-run.md +11 -11
  78. package/skills/ai-research/index/6-write.md +23 -23
  79. package/skills/ai-research/library.json +104 -104
  80. package/skills/analyze-app/SKILL.md +17 -12
  81. package/skills/design-contract/SKILL.md +42 -7
  82. package/skills/design-contract/data-views.md +2 -2
  83. package/skills/design-library/EVALS.md +1 -1
  84. package/skills/design-library/INDEX.md +39 -39
  85. package/skills/design-library/SKILL.md +19 -13
  86. package/skills/design-library/SOURCE +45 -43
  87. package/skills/feature-audit/SKILL.md +20 -14
  88. package/skills/hallmark/LICENSE +21 -0
  89. package/skills/hallmark/SKILL.md +154 -0
  90. package/skills/hallmark/SOURCE +207 -0
  91. package/skills/hallmark/agents/openai.yaml +13 -0
  92. package/skills/hallmark/references/anatomy.md +71 -0
  93. package/skills/hallmark/references/brand-truth.md +40 -0
  94. package/skills/hallmark/references/comp.md +104 -0
  95. package/skills/hallmark/references/craft.md +116 -0
  96. package/skills/hallmark/references/dark-mode.md +49 -0
  97. package/skills/hallmark/references/data-viz.md +48 -0
  98. package/skills/hallmark/references/design-md.md +53 -0
  99. package/skills/hallmark/references/direction.md +52 -0
  100. package/skills/hallmark/references/export-formats.md +80 -0
  101. package/skills/hallmark/references/flow.md +131 -0
  102. package/skills/hallmark/references/fonts.md +58 -0
  103. package/skills/hallmark/references/gates.md +243 -0
  104. package/skills/hallmark/references/genres/atmospheric.md +24 -0
  105. package/skills/hallmark/references/genres/editorial.md +24 -0
  106. package/skills/hallmark/references/genres/modern-minimal.md +24 -0
  107. package/skills/hallmark/references/genres/playful.md +24 -0
  108. package/skills/hallmark/references/imagery.md +99 -0
  109. package/skills/hallmark/references/interaction-and-states.md +89 -0
  110. package/skills/hallmark/references/layout.md +104 -0
  111. package/skills/hallmark/references/macrostructures/01-bento-grid.md +21 -0
  112. package/skills/hallmark/references/macrostructures/02-long-document.md +35 -0
  113. package/skills/hallmark/references/macrostructures/03-marquee-hero.md +21 -0
  114. package/skills/hallmark/references/macrostructures/04-stat-led.md +21 -0
  115. package/skills/hallmark/references/macrostructures/05-workbench.md +21 -0
  116. package/skills/hallmark/references/macrostructures/06-conversational-faq.md +28 -0
  117. package/skills/hallmark/references/macrostructures/07-manifesto.md +28 -0
  118. package/skills/hallmark/references/macrostructures/08-photographic.md +21 -0
  119. package/skills/hallmark/references/macrostructures/09-quote-led.md +21 -0
  120. package/skills/hallmark/references/macrostructures/10-specimen.md +21 -0
  121. package/skills/hallmark/references/macrostructures/11-catalogue.md +21 -0
  122. package/skills/hallmark/references/macrostructures/12-letter.md +21 -0
  123. package/skills/hallmark/references/macrostructures/13-index-first.md +21 -0
  124. package/skills/hallmark/references/macrostructures/14-narrative-workflow.md +21 -0
  125. package/skills/hallmark/references/macrostructures/15-split-studio.md +21 -0
  126. package/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
  127. package/skills/hallmark/references/macrostructures/17-type-specimen.md +19 -0
  128. package/skills/hallmark/references/macrostructures/18-portfolio-grid.md +21 -0
  129. package/skills/hallmark/references/macrostructures/19-map-diagram.md +21 -0
  130. package/skills/hallmark/references/macrostructures/20-ecosystem-index.md +19 -0
  131. package/skills/hallmark/references/macrostructures/21-component-playground.md +21 -0
  132. package/skills/hallmark/references/macrostructures.md +119 -0
  133. package/skills/hallmark/references/microinteractions.md +80 -0
  134. package/skills/hallmark/references/patterns.md +202 -0
  135. package/skills/hallmark/references/redesign.md +88 -0
  136. package/skills/hallmark/references/responsive.md +46 -0
  137. package/skills/hallmark/references/review-finish.md +19 -0
  138. package/skills/hallmark/references/review-hero.md +20 -0
  139. package/skills/hallmark/references/scroll-choreography.md +110 -0
  140. package/skills/hallmark/references/texture.md +90 -0
  141. package/skills/hallmark/references/themes/README.md +58 -0
  142. package/skills/hallmark/references/themes/almanac.jpg +0 -0
  143. package/skills/hallmark/references/themes/almanac.md +103 -0
  144. package/skills/hallmark/references/themes/arcade.jpg +0 -0
  145. package/skills/hallmark/references/themes/arcade.md +102 -0
  146. package/skills/hallmark/references/themes/atelier.jpg +0 -0
  147. package/skills/hallmark/references/themes/atelier.md +98 -0
  148. package/skills/hallmark/references/themes/bloom.jpg +0 -0
  149. package/skills/hallmark/references/themes/bloom.md +102 -0
  150. package/skills/hallmark/references/themes/blueprint.jpg +0 -0
  151. package/skills/hallmark/references/themes/blueprint.md +103 -0
  152. package/skills/hallmark/references/themes/broadside.jpg +0 -0
  153. package/skills/hallmark/references/themes/broadside.md +108 -0
  154. package/skills/hallmark/references/themes/brutal.jpg +0 -0
  155. package/skills/hallmark/references/themes/brutal.md +95 -0
  156. package/skills/hallmark/references/themes/carnival.jpg +0 -0
  157. package/skills/hallmark/references/themes/carnival.md +104 -0
  158. package/skills/hallmark/references/themes/cobalt.jpg +0 -0
  159. package/skills/hallmark/references/themes/cobalt.md +101 -0
  160. package/skills/hallmark/references/themes/console.jpg +0 -0
  161. package/skills/hallmark/references/themes/console.md +99 -0
  162. package/skills/hallmark/references/themes/coral.jpg +0 -0
  163. package/skills/hallmark/references/themes/coral.md +102 -0
  164. package/skills/hallmark/references/themes/darkroom.jpg +0 -0
  165. package/skills/hallmark/references/themes/darkroom.md +99 -0
  166. package/skills/hallmark/references/themes/dose.jpg +0 -0
  167. package/skills/hallmark/references/themes/dose.md +98 -0
  168. package/skills/hallmark/references/themes/dusk.jpg +0 -0
  169. package/skills/hallmark/references/themes/dusk.md +105 -0
  170. package/skills/hallmark/references/themes/editorial.jpg +0 -0
  171. package/skills/hallmark/references/themes/editorial.md +97 -0
  172. package/skills/hallmark/references/themes/formwork.jpg +0 -0
  173. package/skills/hallmark/references/themes/formwork.md +107 -0
  174. package/skills/hallmark/references/themes/furrow.jpg +0 -0
  175. package/skills/hallmark/references/themes/furrow.md +98 -0
  176. package/skills/hallmark/references/themes/garden.jpg +0 -0
  177. package/skills/hallmark/references/themes/garden.md +97 -0
  178. package/skills/hallmark/references/themes/grid.jpg +0 -0
  179. package/skills/hallmark/references/themes/grid.md +101 -0
  180. package/skills/hallmark/references/themes/halftone.jpg +0 -0
  181. package/skills/hallmark/references/themes/halftone.md +106 -0
  182. package/skills/hallmark/references/themes/hedgerow.jpg +0 -0
  183. package/skills/hallmark/references/themes/hedgerow.md +104 -0
  184. package/skills/hallmark/references/themes/hum.jpg +0 -0
  185. package/skills/hallmark/references/themes/hum.md +99 -0
  186. package/skills/hallmark/references/themes/index.json +2191 -0
  187. package/skills/hallmark/references/themes/ink.jpg +0 -0
  188. package/skills/hallmark/references/themes/ink.md +106 -0
  189. package/skills/hallmark/references/themes/kiln.jpg +0 -0
  190. package/skills/hallmark/references/themes/kiln.md +108 -0
  191. package/skills/hallmark/references/themes/loom.jpg +0 -0
  192. package/skills/hallmark/references/themes/loom.md +106 -0
  193. package/skills/hallmark/references/themes/lumen.jpg +0 -0
  194. package/skills/hallmark/references/themes/lumen.md +103 -0
  195. package/skills/hallmark/references/themes/manifesto.jpg +0 -0
  196. package/skills/hallmark/references/themes/manifesto.md +101 -0
  197. package/skills/hallmark/references/themes/midnight.jpg +0 -0
  198. package/skills/hallmark/references/themes/midnight.md +97 -0
  199. package/skills/hallmark/references/themes/newsprint.jpg +0 -0
  200. package/skills/hallmark/references/themes/newsprint.md +102 -0
  201. package/skills/hallmark/references/themes/overprint.jpg +0 -0
  202. package/skills/hallmark/references/themes/overprint.md +101 -0
  203. package/skills/hallmark/references/themes/pasteup.jpg +0 -0
  204. package/skills/hallmark/references/themes/pasteup.md +100 -0
  205. package/skills/hallmark/references/themes/piste.jpg +0 -0
  206. package/skills/hallmark/references/themes/piste.md +99 -0
  207. package/skills/hallmark/references/themes/ribbon.jpg +0 -0
  208. package/skills/hallmark/references/themes/ribbon.md +106 -0
  209. package/skills/hallmark/references/themes/rim.jpg +0 -0
  210. package/skills/hallmark/references/themes/rim.md +96 -0
  211. package/skills/hallmark/references/themes/riso.jpg +0 -0
  212. package/skills/hallmark/references/themes/riso.md +98 -0
  213. package/skills/hallmark/references/themes/solstice.jpg +0 -0
  214. package/skills/hallmark/references/themes/solstice.md +104 -0
  215. package/skills/hallmark/references/themes/specimen.jpg +0 -0
  216. package/skills/hallmark/references/themes/specimen.md +100 -0
  217. package/skills/hallmark/references/themes/spore.jpg +0 -0
  218. package/skills/hallmark/references/themes/spore.md +104 -0
  219. package/skills/hallmark/references/themes/sport.jpg +0 -0
  220. package/skills/hallmark/references/themes/sport.md +101 -0
  221. package/skills/hallmark/references/themes/squeeze.jpg +0 -0
  222. package/skills/hallmark/references/themes/squeeze.md +99 -0
  223. package/skills/hallmark/references/themes/sticker.jpg +0 -0
  224. package/skills/hallmark/references/themes/sticker.md +106 -0
  225. package/skills/hallmark/references/themes/studio.jpg +0 -0
  226. package/skills/hallmark/references/themes/studio.md +99 -0
  227. package/skills/hallmark/references/themes/terminal.jpg +0 -0
  228. package/skills/hallmark/references/themes/terminal.md +96 -0
  229. package/skills/hallmark/references/themes/terrazzo.jpg +0 -0
  230. package/skills/hallmark/references/themes/terrazzo.md +106 -0
  231. package/skills/hallmark/references/themes/ultraviolet.jpg +0 -0
  232. package/skills/hallmark/references/themes/ultraviolet.md +106 -0
  233. package/skills/hallmark/references/themes/vellum.jpg +0 -0
  234. package/skills/hallmark/references/themes/vellum.md +105 -0
  235. package/skills/hallmark/references/themes/wayfind.jpg +0 -0
  236. package/skills/hallmark/references/themes/wayfind.md +105 -0
  237. package/skills/hallmark/references/themes/werkstatt.jpg +0 -0
  238. package/skills/hallmark/references/themes/werkstatt.md +105 -0
  239. package/skills/hallmark/scripts/capture.mjs +125 -0
  240. package/skills/hallmark/scripts/fixtures/exemplars/atmospheric.html +324 -0
  241. package/skills/hallmark/scripts/fixtures/exemplars/editorial.html +363 -0
  242. package/skills/hallmark/scripts/fixtures/exemplars/modern-minimal.html +527 -0
  243. package/skills/hallmark/scripts/fixtures/exemplars/playful.html +353 -0
  244. package/skills/hallmark/scripts/fixtures/eyebrow-fail/index.html +118 -0
  245. package/skills/hallmark/scripts/fixtures/eyebrow-pass/index.html +137 -0
  246. package/skills/hallmark/scripts/fixtures/kept/after.html +27 -0
  247. package/skills/hallmark/scripts/fixtures/kept/before.html +26 -0
  248. package/skills/hallmark/scripts/fixtures/slopped.html +145 -0
  249. package/skills/hallmark/scripts/fixtures/tailwind-fail/Feature.vue +14 -0
  250. package/skills/hallmark/scripts/fixtures/tailwind-fail/Hero.tsx +33 -0
  251. package/skills/hallmark/scripts/fixtures/tailwind-fail/index.html +74 -0
  252. package/skills/hallmark/scripts/fixtures/tailwind-fail/style.css +6 -0
  253. package/skills/hallmark/scripts/fixtures/tailwind-pass/Hero.tsx +33 -0
  254. package/skills/hallmark/scripts/fixtures/tailwind-pass/index.html +57 -0
  255. package/skills/hallmark/scripts/fixtures/tailwind-pass/style.css +15 -0
  256. package/skills/hallmark/scripts/fixtures/waiver/index.html +54 -0
  257. package/skills/hallmark/scripts/imagegen.mjs +85 -0
  258. package/skills/hallmark/scripts/install-hook.mjs +70 -0
  259. package/skills/hallmark/scripts/install.mjs +141 -0
  260. package/skills/hallmark/scripts/lint-hook.mjs +49 -0
  261. package/skills/hallmark/scripts/photos.json +1 -0
  262. package/skills/hallmark/scripts/picture.mjs +71 -0
  263. package/skills/hallmark/scripts/refcheck.mjs +352 -0
  264. package/skills/hallmark/scripts/sloplint-render.mjs +1252 -0
  265. package/skills/hallmark/scripts/sloplint.mjs +4422 -0
  266. package/skills/hallmark/scripts/theme.mjs +650 -0
  267. package/skills/harness/SKILL.md +15 -8
  268. package/skills/i-have-adhd/SKILL.md +22 -11
  269. package/skills/no-ai-slop/SKILL.md +15 -1
  270. package/skills/plan-add/SKILL.md +33 -26
  271. package/skills/plan-status/SKILL.md +13 -7
  272. package/skills/plan-sync/SKILL.md +18 -12
  273. package/skills/pm-frameworks/SKILL.md +19 -10
  274. package/skills/pm-frameworks/SOURCE +7 -5
  275. package/skills/ponytail/SKILL.md +19 -14
  276. package/skills/product-analysis/SKILL.md +22 -18
  277. package/skills/product-flow/SKILL.md +14 -9
  278. package/skills/research/SKILL.md +75 -39
  279. package/skills/rl-env/SKILL.md +25 -13
  280. package/skills/rl-env/SOURCE +8 -6
  281. package/skills/tezgah-contract/SKILL.md +47 -42
  282. package/bin/tezgah-skill.py +0 -59
  283. /package/skills/ai-research/0-autoresearch-skill/{SKILL.md → ENTRY.md} +0 -0
  284. /package/skills/ai-research/01-model-architecture/litgpt/{SKILL.md → ENTRY.md} +0 -0
  285. /package/skills/ai-research/01-model-architecture/mamba/{SKILL.md → ENTRY.md} +0 -0
  286. /package/skills/ai-research/01-model-architecture/nanogpt/{SKILL.md → ENTRY.md} +0 -0
  287. /package/skills/ai-research/01-model-architecture/rwkv/{SKILL.md → ENTRY.md} +0 -0
  288. /package/skills/ai-research/01-model-architecture/torchtitan/{SKILL.md → ENTRY.md} +0 -0
  289. /package/skills/ai-research/02-tokenization/huggingface-tokenizers/{SKILL.md → ENTRY.md} +0 -0
  290. /package/skills/ai-research/02-tokenization/sentencepiece/{SKILL.md → ENTRY.md} +0 -0
  291. /package/skills/ai-research/03-fine-tuning/axolotl/{SKILL.md → ENTRY.md} +0 -0
  292. /package/skills/ai-research/03-fine-tuning/llama-factory/{SKILL.md → ENTRY.md} +0 -0
  293. /package/skills/ai-research/03-fine-tuning/peft/{SKILL.md → ENTRY.md} +0 -0
  294. /package/skills/ai-research/03-fine-tuning/unsloth/{SKILL.md → ENTRY.md} +0 -0
  295. /package/skills/ai-research/04-mechanistic-interpretability/nnsight/{SKILL.md → ENTRY.md} +0 -0
  296. /package/skills/ai-research/04-mechanistic-interpretability/pyvene/{SKILL.md → ENTRY.md} +0 -0
  297. /package/skills/ai-research/04-mechanistic-interpretability/saelens/{SKILL.md → ENTRY.md} +0 -0
  298. /package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/{SKILL.md → ENTRY.md} +0 -0
  299. /package/skills/ai-research/05-data-processing/nemo-curator/{SKILL.md → ENTRY.md} +0 -0
  300. /package/skills/ai-research/05-data-processing/ray-data/{SKILL.md → ENTRY.md} +0 -0
  301. /package/skills/ai-research/06-post-training/grpo-rl-training/{SKILL.md → ENTRY.md} +0 -0
  302. /package/skills/ai-research/06-post-training/miles/{SKILL.md → ENTRY.md} +0 -0
  303. /package/skills/ai-research/06-post-training/openrlhf/{SKILL.md → ENTRY.md} +0 -0
  304. /package/skills/ai-research/06-post-training/simpo/{SKILL.md → ENTRY.md} +0 -0
  305. /package/skills/ai-research/06-post-training/slime/{SKILL.md → ENTRY.md} +0 -0
  306. /package/skills/ai-research/06-post-training/torchforge/{SKILL.md → ENTRY.md} +0 -0
  307. /package/skills/ai-research/06-post-training/trl-fine-tuning/{SKILL.md → ENTRY.md} +0 -0
  308. /package/skills/ai-research/06-post-training/verl/{SKILL.md → ENTRY.md} +0 -0
  309. /package/skills/ai-research/07-safety-alignment/constitutional-ai/{SKILL.md → ENTRY.md} +0 -0
  310. /package/skills/ai-research/07-safety-alignment/llamaguard/{SKILL.md → ENTRY.md} +0 -0
  311. /package/skills/ai-research/07-safety-alignment/nemo-guardrails/{SKILL.md → ENTRY.md} +0 -0
  312. /package/skills/ai-research/07-safety-alignment/prompt-guard/{SKILL.md → ENTRY.md} +0 -0
  313. /package/skills/ai-research/08-distributed-training/accelerate/{SKILL.md → ENTRY.md} +0 -0
  314. /package/skills/ai-research/08-distributed-training/deepspeed/{SKILL.md → ENTRY.md} +0 -0
  315. /package/skills/ai-research/08-distributed-training/megatron-core/{SKILL.md → ENTRY.md} +0 -0
  316. /package/skills/ai-research/08-distributed-training/pytorch-fsdp2/{SKILL.md → ENTRY.md} +0 -0
  317. /package/skills/ai-research/08-distributed-training/pytorch-lightning/{SKILL.md → ENTRY.md} +0 -0
  318. /package/skills/ai-research/08-distributed-training/ray-train/{SKILL.md → ENTRY.md} +0 -0
  319. /package/skills/ai-research/09-infrastructure/lambda-labs/{SKILL.md → ENTRY.md} +0 -0
  320. /package/skills/ai-research/09-infrastructure/modal/{SKILL.md → ENTRY.md} +0 -0
  321. /package/skills/ai-research/09-infrastructure/skypilot/{SKILL.md → ENTRY.md} +0 -0
  322. /package/skills/ai-research/10-optimization/awq/{SKILL.md → ENTRY.md} +0 -0
  323. /package/skills/ai-research/10-optimization/bitsandbytes/{SKILL.md → ENTRY.md} +0 -0
  324. /package/skills/ai-research/10-optimization/flash-attention/{SKILL.md → ENTRY.md} +0 -0
  325. /package/skills/ai-research/10-optimization/gguf/{SKILL.md → ENTRY.md} +0 -0
  326. /package/skills/ai-research/10-optimization/gptq/{SKILL.md → ENTRY.md} +0 -0
  327. /package/skills/ai-research/10-optimization/hqq/{SKILL.md → ENTRY.md} +0 -0
  328. /package/skills/ai-research/10-optimization/ml-training-recipes/{SKILL.md → ENTRY.md} +0 -0
  329. /package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
  330. /package/skills/ai-research/11-evaluation/lm-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
  331. /package/skills/ai-research/11-evaluation/nemo-evaluator/{SKILL.md → ENTRY.md} +0 -0
  332. /package/skills/ai-research/12-inference-serving/llama-cpp/{SKILL.md → ENTRY.md} +0 -0
  333. /package/skills/ai-research/12-inference-serving/sglang/{SKILL.md → ENTRY.md} +0 -0
  334. /package/skills/ai-research/12-inference-serving/tensorrt-llm/{SKILL.md → ENTRY.md} +0 -0
  335. /package/skills/ai-research/12-inference-serving/vllm/{SKILL.md → ENTRY.md} +0 -0
  336. /package/skills/ai-research/13-mlops/mlflow/{SKILL.md → ENTRY.md} +0 -0
  337. /package/skills/ai-research/13-mlops/swanlab/{SKILL.md → ENTRY.md} +0 -0
  338. /package/skills/ai-research/13-mlops/tensorboard/{SKILL.md → ENTRY.md} +0 -0
  339. /package/skills/ai-research/13-mlops/weights-and-biases/{SKILL.md → ENTRY.md} +0 -0
  340. /package/skills/ai-research/14-agents/a-evolve/{SKILL.md → ENTRY.md} +0 -0
  341. /package/skills/ai-research/14-agents/autogpt/{SKILL.md → ENTRY.md} +0 -0
  342. /package/skills/ai-research/14-agents/crewai/{SKILL.md → ENTRY.md} +0 -0
  343. /package/skills/ai-research/14-agents/langchain/{SKILL.md → ENTRY.md} +0 -0
  344. /package/skills/ai-research/14-agents/llamaindex/{SKILL.md → ENTRY.md} +0 -0
  345. /package/skills/ai-research/15-rag/chroma/{SKILL.md → ENTRY.md} +0 -0
  346. /package/skills/ai-research/15-rag/faiss/{SKILL.md → ENTRY.md} +0 -0
  347. /package/skills/ai-research/15-rag/pinecone/{SKILL.md → ENTRY.md} +0 -0
  348. /package/skills/ai-research/15-rag/qdrant/{SKILL.md → ENTRY.md} +0 -0
  349. /package/skills/ai-research/15-rag/sentence-transformers/{SKILL.md → ENTRY.md} +0 -0
  350. /package/skills/ai-research/16-prompt-engineering/dspy/{SKILL.md → ENTRY.md} +0 -0
  351. /package/skills/ai-research/16-prompt-engineering/guidance/{SKILL.md → ENTRY.md} +0 -0
  352. /package/skills/ai-research/16-prompt-engineering/instructor/{SKILL.md → ENTRY.md} +0 -0
  353. /package/skills/ai-research/16-prompt-engineering/outlines/{SKILL.md → ENTRY.md} +0 -0
  354. /package/skills/ai-research/17-observability/langsmith/{SKILL.md → ENTRY.md} +0 -0
  355. /package/skills/ai-research/17-observability/phoenix/{SKILL.md → ENTRY.md} +0 -0
  356. /package/skills/ai-research/18-multimodal/audiocraft/{SKILL.md → ENTRY.md} +0 -0
  357. /package/skills/ai-research/18-multimodal/blip-2/{SKILL.md → ENTRY.md} +0 -0
  358. /package/skills/ai-research/18-multimodal/clip/{SKILL.md → ENTRY.md} +0 -0
  359. /package/skills/ai-research/18-multimodal/cosmos-policy/{SKILL.md → ENTRY.md} +0 -0
  360. /package/skills/ai-research/18-multimodal/llava/{SKILL.md → ENTRY.md} +0 -0
  361. /package/skills/ai-research/18-multimodal/openpi/{SKILL.md → ENTRY.md} +0 -0
  362. /package/skills/ai-research/18-multimodal/openvla-oft/{SKILL.md → ENTRY.md} +0 -0
  363. /package/skills/ai-research/18-multimodal/segment-anything/{SKILL.md → ENTRY.md} +0 -0
  364. /package/skills/ai-research/18-multimodal/stable-diffusion/{SKILL.md → ENTRY.md} +0 -0
  365. /package/skills/ai-research/18-multimodal/whisper/{SKILL.md → ENTRY.md} +0 -0
  366. /package/skills/ai-research/19-emerging-techniques/knowledge-distillation/{SKILL.md → ENTRY.md} +0 -0
  367. /package/skills/ai-research/19-emerging-techniques/long-context/{SKILL.md → ENTRY.md} +0 -0
  368. /package/skills/ai-research/19-emerging-techniques/model-merging/{SKILL.md → ENTRY.md} +0 -0
  369. /package/skills/ai-research/19-emerging-techniques/model-pruning/{SKILL.md → ENTRY.md} +0 -0
  370. /package/skills/ai-research/19-emerging-techniques/moe-training/{SKILL.md → ENTRY.md} +0 -0
  371. /package/skills/ai-research/19-emerging-techniques/speculative-decoding/{SKILL.md → ENTRY.md} +0 -0
  372. /package/skills/ai-research/20-ml-paper-writing/academic-plotting/{SKILL.md → ENTRY.md} +0 -0
  373. /package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/{SKILL.md → ENTRY.md} +0 -0
  374. /package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/{SKILL.md → ENTRY.md} +0 -0
  375. /package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/{SKILL.md → ENTRY.md} +0 -0
  376. /package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/{SKILL.md → ENTRY.md} +0 -0
  377. /package/skills/ai-research/21-research-ideation/creative-thinking-for-research/{SKILL.md → ENTRY.md} +0 -0
  378. /package/skills/ai-research/22-agent-native-research-artifact/compiler/{SKILL.md → ENTRY.md} +0 -0
  379. /package/skills/ai-research/22-agent-native-research-artifact/research-manager/{SKILL.md → ENTRY.md} +0 -0
  380. /package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/{SKILL.md → ENTRY.md} +0 -0
  381. /package/skills/design-library/adaptive-interfaces/skills/colour-independence/{SKILL.md → ENTRY.md} +0 -0
  382. /package/skills/design-library/adaptive-interfaces/skills/flexible-typography/{SKILL.md → ENTRY.md} +0 -0
  383. /package/skills/design-library/adaptive-interfaces/skills/information-density/{SKILL.md → ENTRY.md} +0 -0
  384. /package/skills/design-library/adaptive-interfaces/skills/preference-audit/{SKILL.md → ENTRY.md} +0 -0
  385. /package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/{SKILL.md → ENTRY.md} +0 -0
  386. /package/skills/design-library/adaptive-interfaces/skills/responsive-review/{SKILL.md → ENTRY.md} +0 -0
  387. /package/skills/design-library/adaptive-interfaces/skills/simplified-views/{SKILL.md → ENTRY.md} +0 -0
  388. /package/skills/design-library/adaptive-interfaces/skills/specify/{SKILL.md → ENTRY.md} +0 -0
  389. /package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/{SKILL.md → ENTRY.md} +0 -0
  390. /package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/{SKILL.md → ENTRY.md} +0 -0
  391. /package/skills/design-library/cognitive-accessibility/skills/assess-load/{SKILL.md → ENTRY.md} +0 -0
  392. /package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/{SKILL.md → ENTRY.md} +0 -0
  393. /package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/{SKILL.md → ENTRY.md} +0 -0
  394. /package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/{SKILL.md → ENTRY.md} +0 -0
  395. /package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/{SKILL.md → ENTRY.md} +0 -0
  396. /package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/{SKILL.md → ENTRY.md} +0 -0
  397. /package/skills/design-library/cognitive-accessibility/skills/plain-language-design/{SKILL.md → ENTRY.md} +0 -0
  398. /package/skills/design-library/cognitive-accessibility/skills/review/{SKILL.md → ENTRY.md} +0 -0
  399. /package/skills/design-library/cognitive-accessibility/skills/simplify/{SKILL.md → ENTRY.md} +0 -0
  400. /package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/{SKILL.md → ENTRY.md} +0 -0
  401. /package/skills/design-library/design-research/skills/behavioural-analytics/{SKILL.md → ENTRY.md} +0 -0
  402. /package/skills/design-library/design-research/skills/qual-quant-triangulation/{SKILL.md → ENTRY.md} +0 -0
  403. /package/skills/design-library/inclusive-interaction/skills/audit/{SKILL.md → ENTRY.md} +0 -0
  404. /package/skills/design-library/inclusive-interaction/skills/design-flow/{SKILL.md → ENTRY.md} +0 -0
  405. /package/skills/design-library/inclusive-interaction/skills/feedback-and-status/{SKILL.md → ENTRY.md} +0 -0
  406. /package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/{SKILL.md → ENTRY.md} +0 -0
  407. /package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/{SKILL.md → ENTRY.md} +0 -0
  408. /package/skills/design-library/inclusive-interaction/skills/keyboard-review/{SKILL.md → ENTRY.md} +0 -0
  409. /package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/{SKILL.md → ENTRY.md} +0 -0
  410. /package/skills/design-library/inclusive-interaction/skills/multi-modal-input/{SKILL.md → ENTRY.md} +0 -0
  411. /package/skills/design-library/inclusive-interaction/skills/touch-target-design/{SKILL.md → ENTRY.md} +0 -0
  412. /package/skills/design-library/inclusive-interaction/skills/voice-interaction/{SKILL.md → ENTRY.md} +0 -0
  413. /package/skills/design-library/visual-critique/skills/critique-affordance/{SKILL.md → ENTRY.md} +0 -0
  414. /package/skills/design-library/visual-critique/skills/critique-brand-consistency/{SKILL.md → ENTRY.md} +0 -0
  415. /package/skills/design-library/visual-critique/skills/critique-color/{SKILL.md → ENTRY.md} +0 -0
  416. /package/skills/design-library/visual-critique/skills/critique-composition/{SKILL.md → ENTRY.md} +0 -0
  417. /package/skills/design-library/visual-critique/skills/critique-information-density/{SKILL.md → ENTRY.md} +0 -0
  418. /package/skills/design-library/visual-critique/skills/critique-typography/{SKILL.md → ENTRY.md} +0 -0
  419. /package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/{SKILL.md → ENTRY.md} +0 -0
  420. /package/skills/pm-frameworks/intended-vs-implemented/{SKILL.md → ENTRY.md} +0 -0
  421. /package/skills/pm-frameworks/opportunity-solution-tree/{SKILL.md → ENTRY.md} +0 -0
  422. /package/skills/rl-env/generate-nemo-gym-env/{SKILL.md → ENTRY.md} +0 -0
  423. /package/skills/rl-env/generate-openenv-env/{SKILL.md → ENTRY.md} +0 -0
  424. /package/skills/rl-env/generate-ors-env/{SKILL.md → ENTRY.md} +0 -0
  425. /package/skills/rl-env/generate-verifiers-env/{SKILL.md → ENTRY.md} +0 -0
  426. /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+(?:-m\s+)?(?:pytest|unittest|mypy|ruff|flake8|compileall|"
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
- r"npm\s+(?:test|t\b)|npm\s+run\s+\S*(?:test|lint|typecheck|check|build|ci)|"
57
- r"(?:yarn|pnpm|bun)\s+(?:test|run\s+\S*(?:test|lint|build|check))|"
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+check\b",
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
- path = str(path or "").strip()
1136
- if not path:
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
- if detail.startswith("blocked"):
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
- if detail[len("blocked: "):] in SHAPE_BLOCKS:
1814
- out["shape_blocked"] += 1
1815
- else:
1816
- # split by the Stop class: which check refused, as a number
1817
- cls = detail[len("blocked: "):]
1818
- out["blocked_claims"][cls] = \
1819
- out["blocked_claims"].get(cls, 0) + 1
1820
- if cls not in LIST_BLOCKS:
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
- def _check_runs(cmd):
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
- if INFO_ARGS.search(args) or (sub and sub.match(args)):
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 verify_command(cmd):
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
- return next((name for name, mode in _check_runs(cmd) if mode == "check"),
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 (i == 0 or cmd[i - 1] in " \t\n;&|()<>"):
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 += 3
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 += 1
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 and newlines kept, read the way bash reads it: `'...'` takes no
2155
- escape, `$'...'` (an unescaped `$` only) and `"..."` do, a `\\` outside
2156
- quotes escapes one character, and `#` starts a comment only at the start of
2157
- a word. So `https://x`, `a#b`, `src/*.py ... lib/*/`, `'x\\'` and `\\$'x\\'`
2158
- are words, not a comment or an open string that blanks the command after
2159
- them (gate-01). A quote left open blanks from itself to the end - bash runs
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
- i, start, prev = i + 2, False, ""
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
- end += 2 if escapes and text[end] == "\\" else 1
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 = ch.isspace() or ch in ";&|()<>", ch
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 "".join(out)
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`), and a comment - `#` at the start of a word -
2261
- dropped to its line end. A double-quoted string holding a `$( )` or a
2262
- backtick runs a command, so its newlines still split - the reading this had
2263
- before, which keeps that command visible. A quote left open at the end goes
2264
- back to a split at every newline from its line on."""
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, start, i, n = [], [], None, True, 0, len(text)
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, start, runs = [], None, True, False
2274
- i += 2 if text[i:i + 2] == "\r\n" else 1
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
- cur.append(text[i:i + 2])
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
- cur.append(text[i:i + 2])
2290
- i, start = i + 2, False
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 start:
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
- start = not quote and (ch.isspace() or ch in ";&|()<>")
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 "")).replace("\\\n", " ")
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
- # the fd before the operator (`2>`), an `&` with its fd (`>&2`),
2332
- # or the target after it - none of them is an argument
2333
- if cur and cur[-1].isdigit():
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
- if cur:
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
- for seg in [cur] + [outer for outer, _open in reversed(subs)]:
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, a newline ends a command only where bash ends
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`. ponytail: `set -e` is not read, so `set -e; pytest; echo
2652
- done` records as ran - a lost credit, never an invented one."""
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
- if not verify_command(shlex.join(words)):
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 = str(cmd or "")
3059
+ raw = joined(cmd)
2689
3060
  pos, check = 0, None
2690
- # mask keeps every offset, so a stage's span in `c` is its text in `raw`
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 untrusted_source(tool, inp):
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
- return not status_hidden(entry.get("detail"))
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 _design_evidence(rows):
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
- def _shell_read(cmd):
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
- elif ch == "#" and (i == 0 or cmd[i - 1] in " \t\n;&|()<>"):
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 _bookkeeping_command(cmd, git=GIT_BOOKKEEPING):
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
- if not worked or quiet:
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 quiet) and not (done or verified) and not external:
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
- "`tezgah-design check` ran over a measurement, so nothing "
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 `tezgah-design check --contract "
5623
+ "that floor. Run `~/.config/tezgah/bin/tezgah-design check --contract "
5077
5624
  ".tezgah/design-contract.md --measured <measurement.json>` "
5078
- "(`tezgah-design derive` writes the contract when the repo "
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 `tezgah-task slice resume %s` - "
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 `tezgah-task slice done "
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`)